source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
+[[package]]
+name = "assert-json-diff"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50f1c3703dd33532d7f0ca049168930e9099ecac238e23cf932f3a69c42f06da"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
[[package]]
name = "async-mutex"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd"
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+[[package]]
+name = "combine"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a909e4d93292cd8e9c42e189f61681eff9d67b6541f96b8a1a737f23737bd001"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
[[package]]
name = "comrak"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a164bb2ceaeff4f42542bdb847c41517c78a60f5649671b2a07312b6e117549"
+[[package]]
+name = "html2md"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61f6bf799d9770725ec13d66f4af9344e96285dc14d8e71e0fe02d272690667f"
+dependencies = [
+ "html5ever 0.25.1",
+ "jni",
+ "lazy_static",
+ "markup5ever_rcdom",
+ "percent-encoding",
+ "regex",
+]
+
[[package]]
name = "html5ever"
version = "0.22.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
+[[package]]
+name = "jni"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec"
+dependencies = [
+ "cesu8",
+ "combine",
+ "jni-sys",
+ "log",
+ "thiserror",
+ "walkdir",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
+
[[package]]
name = "jpeg-decoder"
version = "0.1.22"
"actix-rt",
"actix-web",
"anyhow",
+ "assert-json-diff",
"async-trait",
"awc",
"background-jobs",
"chrono",
"diesel",
"futures",
+ "html2md",
"http",
"http-signature-normalization-actix",
"itertools",
"serde",
"serde_json",
"serde_with",
+ "serial_test",
"sha2",
"strum",
"strum_macros",
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
[[package]]
name = "schannel"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
+[[package]]
+name = "walkdir"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
+dependencies = [
+ "same-file",
+ "winapi",
+ "winapi-util",
+]
+
[[package]]
name = "want"
version = "0.3.0"
# https://join-lemmy.org/docs/en/administration/configuration.html
{
hostname: lemmy-alpha
+ federation: {
+ enabled: true
+ }
}
thiserror = "1.0.29"
background-jobs = "0.9.0"
reqwest = { version = "0.11.4", features = ["json"] }
+html2md = "0.2.13"
+[dev-dependencies]
+serial_test = "0.5.1"
+assert-json-diff = "2.0.1"
\ No newline at end of file
--- /dev/null
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ {
+ "comments_enabled": {
+ "id": "pt:commentsEnabled",
+ "type": "sc:Boolean"
+ },
+ "matrixUserId": {
+ "id": "as:alsoKnownAs",
+ "type": "sc:Text"
+ },
+ "moderators": "as:moderators",
+ "pt": "https://join-lemmy.org#",
+ "sc": "http://schema.org#",
+ "sensitive": "as:sensitive",
+ "stickied": "as:stickied"
+ },
+ "https://w3id.org/security/v1"
+ ],
+ "attributedTo": "https://lemmy.ml/u/nutomic",
+ "content": "While I very much get and respect the general sentiment, I think from the perspective of a Central European non-English person in a country with a significant number of, also non-English speaking Nazis, the current approach of filtering slurs based on an English regex is fatally flawed. You can happily use Lemmy to create a hostile far right community where everyone is easily able to use whatever hurtful slurs they want as long as they are not the few specifically blocked English ones. \n\nOn the other hand you create a situation where people feel the need to question the choice of software of their community because they read about censorship or whatever to be used in Lemmy and might stay away and move to other software even though the would maybe never be affected by the slur-filter as the number is not so large and the overlap with other languages not very big.\n\nSo I would argue that this specific implementation of a slur-filter just doesn't achieve what it aims to achieve and should be fundamentally rethought, maybe as configurable per instance.",
+ "id": "https://lemmy.ml/comment/38741",
+ "inReplyTo": [
+ "https://lemmy.ml/post/55143"
+ ],
+ "mediaType": "text/html",
+ "published": "2021-03-01T13:42:43.966208+00:00",
+ "source": {
+ "content": "While I very much get and respect the general sentiment, I think from the perspective of a Central European non-English person in a country with a significant number of, also non-English speaking Nazis, the current approach of filtering slurs based on an English regex is fatally flawed. You can happily use Lemmy to create a hostile far right community where everyone is easily able to use whatever hurtful slurs they want as long as they are not the few specifically blocked English ones. \n\nOn the other hand you create a situation where people feel the need to question the choice of software of their community because they read about censorship or whatever to be used in Lemmy and might stay away and move to other software even though the would maybe never be affected by the slur-filter as the number is not so large and the overlap with other languages not very big.\n\nSo I would argue that this specific implementation of a slur-filter just doesn't achieve what it aims to achieve and should be fundamentally rethought, maybe as configurable per instance.",
+ "mediaType": "text/markdown"
+ },
+ "to": ["https://www.w3.org/ns/activitystreams#Public"],
+ "type": "Note",
+ "updated": "2021-03-01T13:43:03.955787+00:00"
+}
--- /dev/null
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ {
+ "comments_enabled": {
+ "id": "pt:commentsEnabled",
+ "type": "sc:Boolean"
+ },
+ "matrixUserId": {
+ "id": "as:alsoKnownAs",
+ "type": "sc:Text"
+ },
+ "moderators": "as:moderators",
+ "pt": "https://join-lemmy.org#",
+ "sc": "http://schema.org#",
+ "sensitive": "as:sensitive",
+ "stickied": "as:stickied"
+ },
+ "https://w3id.org/security/v1"
+ ],
+ "content": "<p>Lemmy Announcements</p>\n<hr />\n<p>Feel free to announce new communities here.</p>\n<p>Other than that, this is <em>reserved for admin use only</em>.</p>\n",
+ "endpoints": {
+ "sharedInbox": "https://lemmy.ml/inbox"
+ },
+ "followers": "https://lemmy.ml/c/announcements/followers",
+ "icon": {
+ "type": "Image",
+ "url": "https://lemmy.ml/pictrs/image/waqyZwLAy4.webp"
+ },
+ "id": "https://lemmy.ml/c/announcements",
+ "inbox": "https://lemmy.ml/c/announcements/inbox",
+ "mediaType": "text/html",
+ "moderators": "https://lemmy.ml/c/announcements/moderators",
+ "name": "Announcements",
+ "outbox": "https://lemmy.ml/c/announcements/outbox",
+ "preferredUsername": "announcements",
+ "publicKey": {
+ "id": "https://lemmy.ml/c/announcements#main-key",
+ "owner": "https://lemmy.ml/c/announcements",
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzRjKTNtvDCmugplwEh+g\nx1bhKm6BHUZfXfpscgMMm7tXFswSDzUQirMgfkxa9ubfr1PDFKffA2vQ9x6CyuO/\n70xTafdOHyV1tSqzgKz0ZvFZ/VCOo6qy1mYWVkrtBm/fKzM+87MdkKYB/zI4VyEJ\nLfLQgjwxBAEYUH3CBG71U0gO0TwbimWNN0vqlfp0QfThNe1WYObF88ZVzMLgFbr7\nRHBItZjlZ/d8foPDidlIR3l2dJjy0EsD8F9JM340jtX7LXqFmU4j1AQKNHTDLnUF\nwYVhzuQGNJ504l5LZkFG54XfIFT7dx2QwuuM9bSnfPv/98RYrq1Si6tCkxEt1cVe\n4wIDAQAB\n-----END PUBLIC KEY-----\n"
+ },
+ "published": "2019-06-02T16:43:50.799554+00:00",
+ "sensitive": false,
+ "source": {
+ "content": "Lemmy Announcements\n\n---\n\nFeel free to announce new communities here.\n\nOther than that, this is *reserved for admin use only*.",
+ "mediaType": "text/markdown"
+ },
+ "type": "Group",
+ "updated": "2021-03-10T17:18:10.498868+00:00"
+}
--- /dev/null
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ {
+ "comments_enabled": {
+ "id": "pt:commentsEnabled",
+ "type": "sc:Boolean"
+ },
+ "matrixUserId": {
+ "id": "as:alsoKnownAs",
+ "type": "sc:Text"
+ },
+ "moderators": "as:moderators",
+ "pt": "https://join-lemmy.org#",
+ "sc": "http://schema.org#",
+ "sensitive": "as:sensitive",
+ "stickied": "as:stickied"
+ },
+ "https://w3id.org/security/v1"
+ ],
+ "content": "<p>Lemmy maintainer. Interested in politics, video games, and many other things.</p>\n",
+ "endpoints": {
+ "sharedInbox": "https://lemmy.ml/inbox"
+ },
+ "icon": {
+ "type": "Image",
+ "url": "https://lemmy.ml/pictrs/image/ed9ej7.jpg"
+ },
+ "id": "https://lemmy.ml/u/nutomic",
+ "inbox": "https://lemmy.ml/u/nutomic/inbox",
+ "mediaType": "text/html",
+ "outbox": "https://lemmy.ml/u/nutomic/outbox",
+ "preferredUsername": "nutomic",
+ "publicKey": {
+ "id": "https://lemmy.ml/u/nutomic#main-key",
+ "owner": "https://lemmy.ml/u/nutomic",
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0lP99/s5Vv+XbPdkeqIJ\nwoD4GFnHmBnBHdEKChEUWfWj1TtioC/rGNoXFQeXQA3Amhy4nxSceiDnUgwkkuQY\nv0MtIW58NzgknEavtllxL+LSds5pg3gANaDIk8UiWTkqXTg0GnlJMpCK1Chen0l/\nszL6DEvUyTSuS5ZYDXFgewF89Pe7U0S15V5U2Harv7AgJYDyxmUL0D1pGuUCRqcE\nl5MTHJjrXeNnH1w2g8aly8YlO/Cr0L51rFg/lBF23vni7ZLv8HbmWh6YpaAf1R8h\nE45zKR7OHqymdjzrg1ITBwovefpwMkVgnJ+Wdr4HPnFlBSkXPoZeM11+Z8L0anzA\nXwIDAQAB\n-----END PUBLIC KEY-----\n"
+ },
+ "published": "2020-01-17T01:38:22.348392+00:00",
+ "source": {
+ "content": "Lemmy maintainer. Interested in politics, video games, and many other things.",
+ "mediaType": "text/markdown"
+ },
+ "type": "Person",
+ "updated": "2021-08-13T00:11:15.941990+00:00"
+}
--- /dev/null
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ {
+ "comments_enabled": {
+ "id": "pt:commentsEnabled",
+ "type": "sc:Boolean"
+ },
+ "matrixUserId": {
+ "id": "as:alsoKnownAs",
+ "type": "sc:Text"
+ },
+ "moderators": "as:moderators",
+ "pt": "https://join-lemmy.org#",
+ "sc": "http://schema.org#",
+ "sensitive": "as:sensitive",
+ "stickied": "as:stickied"
+ },
+ "https://w3id.org/security/v1"
+ ],
+ "attributedTo": "https://lemmy.ml/u/nutomic",
+ "commentsEnabled": true,
+ "content": "<p>Recently there have been some discussions about the political stances of the Lemmy developers and site admins. To clear up some misconceptions: Lemmy is run by a team of people with different ideologies, including anti-capitalist, communist, anarchist, and others. While @dessalines and I are communists, we take decisions collectively, and don't demand that anyone adopt our views or convert to our ideologies. We wouldn't devote so much time to building a federated site otherwise.</p>\n<p>What's important to us is that you follow the site rules and <a href=\"https://join.lemmy.ml/docs/en/code_of_conduct.html\">Code of Conduct</a>. Meaning primarily, no-bigotry, and being respectful towards others. As long as that is the case, we can get along perfectly fine.</p>\n<p>In general we are open for constructive feedback, so please contact any member of the admin team if you have an idea how to improve Lemmy.</p>\n<h2>Slur Filter</h2>\n<p>We also noticed a consistent criticism of the built-in slur filter in Lemmy. Not so much on lemmy.ml itself, but whenever Lemmy is recommended elsewhere, a few usual suspects keep bringing it up. To these people we say the following: we are using the slur filter as a tool to keep a friendly atmosphere, and prevent racists, sexists and other bigots from using Lemmy. Its existence alone has lead many of them to not make an account, or run an instance: a clear net positive.</p>\n<p>You can see for yourself the words which are blocked (content warning, <a href=\"https://github.com/LemmyNet/lemmy/blob/main/crates/utils/src/utils.rs#L10\">link here</a>). Note that it doesn't include any simple swear words, but only slurs which are used to insult and attack other people. If you want to use any of these words, then please stay on one of the many platforms that permit them. Lemmy is not for you, and we don't want you here.</p>\n<p>We are fully aware that the slur filter is not perfect. It is made for American English, and can give false positives in other languages or dialects. We are totally willing to fix such problems on a case by case basis, simply open an issue in our <a href=\"https://github.com/LemmyNet/lemmy/issues\">repo </a>with a description of the problem.</p>\n",
+ "id": "https://lemmy.ml/post/55143",
+ "mediaType": "text/html",
+ "name": "Statement on Politics of Lemmy.ml",
+ "published": "2021-02-26T12:35:34.292626+00:00",
+ "sensitive": false,
+ "source": {
+ "content": "Recently there have been some discussions about the political stances of the Lemmy developers and site admins. To clear up some misconceptions: Lemmy is run by a team of people with different ideologies, including anti-capitalist, communist, anarchist, and others. While @dessalines and I are communists, we take decisions collectively, and don't demand that anyone adopt our views or convert to our ideologies. We wouldn't devote so much time to building a federated site otherwise.\n\nWhat's important to us is that you follow the site rules and [Code of Conduct](https://join.lemmy.ml/docs/en/code_of_conduct.html). Meaning primarily, no-bigotry, and being respectful towards others. As long as that is the case, we can get along perfectly fine.\n\nIn general we are open for constructive feedback, so please contact any member of the admin team if you have an idea how to improve Lemmy. \n\n## Slur Filter\n\nWe also noticed a consistent criticism of the built-in slur filter in Lemmy. Not so much on lemmy.ml itself, but whenever Lemmy is recommended elsewhere, a few usual suspects keep bringing it up. To these people we say the following: we are using the slur filter as a tool to keep a friendly atmosphere, and prevent racists, sexists and other bigots from using Lemmy. Its existence alone has lead many of them to not make an account, or run an instance: a clear net positive.\n\nYou can see for yourself the words which are blocked (content warning, [link here](https://github.com/LemmyNet/lemmy/blob/main/crates/utils/src/utils.rs#L10)). Note that it doesn't include any simple swear words, but only slurs which are used to insult and attack other people. If you want to use any of these words, then please stay on one of the many platforms that permit them. Lemmy is not for you, and we don't want you here.\n\nWe are fully aware that the slur filter is not perfect. It is made for American English, and can give false positives in other languages or dialects. We are totally willing to fix such problems on a case by case basis, simply open an issue in our [repo ](https://github.com/LemmyNet/lemmy/issues)with a description of the problem.",
+ "mediaType": "text/markdown"
+ },
+ "stickied": true,
+ "to": [
+ "https://lemmy.ml/c/announcements",
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "type": "Page"
+}
--- /dev/null
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ {
+ "comments_enabled": {
+ "id": "pt:commentsEnabled",
+ "type": "sc:Boolean"
+ },
+ "matrixUserId": {
+ "id": "as:alsoKnownAs",
+ "type": "sc:Text"
+ },
+ "moderators": "as:moderators",
+ "pt": "https://join-lemmy.org#",
+ "sc": "http://schema.org#",
+ "sensitive": "as:sensitive",
+ "stickied": "as:stickied"
+ },
+ "https://w3id.org/security/v1"
+ ],
+ "attributedTo": "https://lemmy.ml/u/nutomic",
+ "content": "test",
+ "id": "https://lemmy.ml/private_message/1621",
+ "mediaType": "text/html",
+ "published": "2021-10-21T10:13:14.597721+00:00",
+ "source": {
+ "content": "test",
+ "mediaType": "text/markdown"
+ },
+ "to": "https://queer.hacktivis.me/users/lanodan",
+ "type": "Note"
+}
\ No newline at end of file
--- /dev/null
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://queer.hacktivis.me/schemas/litepub-0.1.jsonld",
+ {
+ "@language": "und"
+ }
+ ],
+ "actor": "https://queer.hacktivis.me/users/lanodan",
+ "attachment": [],
+ "attributedTo": "https://queer.hacktivis.me/users/lanodan",
+ "cc": [
+ "https://www.w3.org/ns/activitystreams#Public"
+ ],
+ "content": "<span class=\"h-card\"><a class=\"u-url mention\" data-user=\"9zkUX4o3WxGM8vGPfU\" href=\"https://pleroma.popolon.org/users/popolon\" rel=\"ugc\">@<span>popolon</span></a></span> Have what?",
+ "context": "https://queer.hacktivis.me/contexts/34cba3d2-2f35-4169-aeff-56af9bfeb753",
+ "conversation": "https://queer.hacktivis.me/contexts/34cba3d2-2f35-4169-aeff-56af9bfeb753",
+ "id": "https://queer.hacktivis.me/objects/8d4973f4-53de-49cd-8c27-df160e16a9c2",
+ "inReplyTo": "https://lemmy.ml/post/55143",
+ "published": "2021-10-07T18:06:52.555500Z",
+ "sensitive": null,
+ "source": "@popolon@pleroma.popolon.org Have what?",
+ "summary": "",
+ "tag": [
+ {
+ "href": "https://pleroma.popolon.org/users/popolon",
+ "name": "@popolon@pleroma.popolon.org",
+ "type": "Mention"
+ }
+ ],
+ "to": [
+ "https://pleroma.popolon.org/users/popolon",
+ "https://queer.hacktivis.me/users/lanodan/followers"
+ ],
+ "type": "Note"
+}
--- /dev/null
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://queer.hacktivis.me/schemas/litepub-0.1.jsonld",
+ {
+ "@language": "und"
+ }
+ ],
+ "alsoKnownAs": [],
+ "attachment": [],
+ "capabilities": {
+ "acceptsChatMessages": true
+ },
+ "discoverable": false,
+ "endpoints": {
+ "oauthAuthorizationEndpoint": "https://queer.hacktivis.me/oauth/authorize",
+ "oauthRegistrationEndpoint": "https://queer.hacktivis.me/api/v1/apps",
+ "oauthTokenEndpoint": "https://queer.hacktivis.me/oauth/token",
+ "sharedInbox": "https://queer.hacktivis.me/inbox",
+ "uploadMedia": "https://queer.hacktivis.me/api/ap/upload_media"
+ },
+ "featured": "https://queer.hacktivis.me/users/lanodan/collections/featured",
+ "followers": "https://queer.hacktivis.me/users/lanodan/followers",
+ "following": "https://queer.hacktivis.me/users/lanodan/following",
+ "icon": {
+ "type": "Image",
+ "url": "https://queer.hacktivis.me/media/d23cf9b0-5586-4592-aca5-9a52777a6042/avatar_HD.png"
+ },
+ "id": "https://queer.hacktivis.me/users/lanodan",
+ "image": {
+ "type": "Image",
+ "url": "https://queer.hacktivis.me/media/37b6ce56-8c24-4e64-bd70-a76e84ab0c69/53a48a3a49ed5e5637a84e4f3663df17f8d764244bbc1027ba03cfc446e8b7bd.jpg"
+ },
+ "inbox": "https://queer.hacktivis.me/users/lanodan/inbox",
+ "manuallyApprovesFollowers": false,
+ "name": "Haelwenn /элвэн/ :bzh: ",
+ "outbox": "https://queer.hacktivis.me/users/lanodan/outbox",
+ "preferredUsername": "lanodan",
+ "publicKey": {
+ "id": "https://queer.hacktivis.me/users/lanodan#main-key",
+ "owner": "https://queer.hacktivis.me/users/lanodan",
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsWOgdjSMc010qvxC3njI\nXJlFWMJ5gJ8QXCW/PajYdsHPM6d+jxBNJ6zp9/tIRa2m7bWHTSkuHQ7QthOpt6vu\n+dAWpKRLS607SPLItn/qUcyXvgN+H8shfyhMxvkVs9jXdtlBsLUVE7UNpN0dxzqe\nI79QWbf7o4amgaIWGRYB+OYMnIxKt+GzIkivZdSVSYjfxNnBYkMCeUxm5EpPIxKS\nP5bBHAVRRambD5NUmyKILuC60/rYuc/C+vmgpY2HCWFS2q6o34dPr9enwL6t4b3m\nS1t/EJHk9rGaaDqSGkDEfyQI83/7SDebWKuETMKKFLZi1vMgQIFuOYCIhN6bIiZm\npQIDAQAB\n-----END PUBLIC KEY-----\n\n"
+ },
+ "summary": "---<br/>Website: <a href=\"https://hacktivis.me/\">https://hacktivis.me/</a><br/>Lang: Français(natif), English(fluent), LSF(🤏~👌), русский (еле-еле), <br/>Politics: Anarchist as in DIY/DIWO, freedom of association, anti-authoritarian, anti-identitarianism<br/><br/>Pronouns: meh, pick any, have fun<br/>Timezone: Let's say Mars, I have a non-24h cycle<br/>```<br/>🦊🦄⚧🂡ⓥ :anarchy: 👿🐧 :gentoo:<br/>Pleroma maintainer (mostly backend)<br/>BadWolf developer<br/>Gentoo contributor<br/><br/>Dayjob: yogoko.fr<br/><br/>That person which uses HJKL in games<br/><br/>Just because computer bad: X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*<br/><br/>banner from: <a href=\"https://soc.flyingcube.tech/objects/56f79be2-9013-4559-9826-f7dc392417db\">https://soc.flyingcube.tech/objects/56f79be2-9013-4559-9826-f7dc392417db</a><br/>Federation-bots: <a class=\"hashtag\" data-tag=\"nobot\" href=\"https://queer.hacktivis.me/tag/nobot\">#nobot</a>",
+ "tag": [
+ {
+ "icon": {
+ "type": "Image",
+ "url": "https://queer.hacktivis.me/emoji/custom/symbols/anarchy.png"
+ },
+ "id": "https://queer.hacktivis.me/emoji/custom/symbols/anarchy.png",
+ "name": ":anarchy:",
+ "type": "Emoji",
+ "updated": "1970-01-01T00:00:00Z"
+ },
+ {
+ "icon": {
+ "type": "Image",
+ "url": "https://queer.hacktivis.me/emoji/custom/mastodon.xyz/bzh.png"
+ },
+ "id": "https://queer.hacktivis.me/emoji/custom/mastodon.xyz/bzh.png",
+ "name": ":bzh:",
+ "type": "Emoji",
+ "updated": "1970-01-01T00:00:00Z"
+ },
+ {
+ "icon": {
+ "type": "Image",
+ "url": "https://queer.hacktivis.me/emoji/custom/gentoo.png"
+ },
+ "id": "https://queer.hacktivis.me/emoji/custom/gentoo.png",
+ "name": ":gentoo:",
+ "type": "Emoji",
+ "updated": "1970-01-01T00:00:00Z"
+ }
+ ],
+ "type": "Person",
+ "url": "https://queer.hacktivis.me/users/lanodan"
+}
},
context::lemmy_context,
fetcher::object_id::ObjectId,
+ migrations::PublicUrlMigration,
objects::{
comment::{ApubComment, Note},
community::ApubCommunity,
use lemmy_apub_lib::{
data::Data,
traits::{ActivityFields, ActivityHandler, ActorType, FromApub, ToApub},
- values::PublicUrl,
verify::verify_domains_match,
};
use lemmy_db_schema::{
#[serde(rename_all = "camelCase")]
pub struct CreateOrUpdateComment {
actor: ObjectId<ApubPerson>,
- to: [PublicUrl; 1],
+ to: PublicUrlMigration,
object: Note,
cc: Vec<Url>,
tag: Vec<Mention>,
let create_or_update = CreateOrUpdateComment {
actor: ObjectId::new(actor.actor_id()),
- to: [PublicUrl::Public],
+ to: PublicUrlMigration::create(),
object: comment.to_apub(context.pool()).await?,
cc: maa.ccs,
tag: maa.tags,
context::lemmy_context,
fetcher::object_id::ObjectId,
generate_moderators_url,
+ migrations::PublicUrlMigration,
objects::{community::ApubCommunity, person::ApubPerson},
};
use activitystreams::{
use lemmy_apub_lib::{
data::Data,
traits::{ActivityFields, ActivityHandler, ActorType},
- values::PublicUrl,
};
use lemmy_db_schema::{
source::community::{CommunityModerator, CommunityModeratorForm},
#[serde(rename_all = "camelCase")]
pub struct AddMod {
actor: ObjectId<ApubPerson>,
- to: [PublicUrl; 1],
+ to: PublicUrlMigration,
object: ObjectId<ApubPerson>,
target: Url,
cc: [ObjectId<ApubCommunity>; 1],
)?;
let add = AddMod {
actor: ObjectId::new(actor.actor_id()),
- to: [PublicUrl::Public],
+ to: PublicUrlMigration::create(),
object: ObjectId::new(added_mod.actor_id()),
target: generate_moderators_url(&community.actor_id)?.into(),
cc: [ObjectId::new(community.actor_id())],
fetcher::object_id::ObjectId,
http::is_activity_already_known,
insert_activity,
+ migrations::PublicUrlMigration,
objects::community::ApubCommunity,
send_lemmy_activity,
CommunityType,
use lemmy_apub_lib::{
data::Data,
traits::{ActivityFields, ActivityHandler, ActorType},
- values::PublicUrl,
};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
#[serde(rename_all = "camelCase")]
pub struct AnnounceActivity {
actor: ObjectId<ApubCommunity>,
- to: [PublicUrl; 1],
+ to: PublicUrlMigration,
object: AnnouncableActivities,
cc: Vec<Url>,
#[serde(rename = "type")]
) -> Result<(), LemmyError> {
let announce = AnnounceActivity {
actor: ObjectId::new(community.actor_id()),
- to: [PublicUrl::Public],
+ to: PublicUrlMigration::create(),
object,
cc: vec![community.followers_url()],
kind: AnnounceType::Announce,
},
context::lemmy_context,
fetcher::object_id::ObjectId,
+ migrations::PublicUrlMigration,
objects::{community::ApubCommunity, person::ApubPerson},
};
use activitystreams::{
use lemmy_apub_lib::{
data::Data,
traits::{ActivityFields, ActivityHandler, ActorType},
- values::PublicUrl,
};
use lemmy_db_schema::{
source::community::{
#[serde(rename_all = "camelCase")]
pub struct BlockUserFromCommunity {
actor: ObjectId<ApubPerson>,
- to: [PublicUrl; 1],
+ to: PublicUrlMigration,
pub(in crate::activities::community) object: ObjectId<ApubPerson>,
cc: [ObjectId<ApubCommunity>; 1],
#[serde(rename = "type")]
) -> Result<BlockUserFromCommunity, LemmyError> {
Ok(BlockUserFromCommunity {
actor: ObjectId::new(actor.actor_id()),
- to: [PublicUrl::Public],
+ to: PublicUrlMigration::create(),
object: ObjectId::new(target.actor_id()),
cc: [ObjectId::new(community.actor_id())],
kind: BlockType::Block,
context::lemmy_context,
fetcher::object_id::ObjectId,
generate_moderators_url,
+ migrations::PublicUrlMigration,
objects::{community::ApubCommunity, person::ApubPerson},
};
use activitystreams::{
use lemmy_apub_lib::{
data::Data,
traits::{ActivityFields, ActivityHandler, ActorType},
- values::PublicUrl,
};
use lemmy_db_schema::{
source::community::{CommunityModerator, CommunityModeratorForm},
#[serde(rename_all = "camelCase")]
pub struct RemoveMod {
actor: ObjectId<ApubPerson>,
- to: [PublicUrl; 1],
+ to: PublicUrlMigration,
pub(in crate::activities) object: ObjectId<ApubPerson>,
cc: [ObjectId<ApubCommunity>; 1],
#[serde(rename = "type")]
)?;
let remove = RemoveMod {
actor: ObjectId::new(actor.actor_id()),
- to: [PublicUrl::Public],
+ to: PublicUrlMigration::create(),
object: ObjectId::new(removed_mod.actor_id()),
target: Some(generate_moderators_url(&community.actor_id)?.into()),
id: id.clone(),
},
context::lemmy_context,
fetcher::object_id::ObjectId,
+ migrations::PublicUrlMigration,
objects::{community::ApubCommunity, person::ApubPerson},
};
use activitystreams::{
use lemmy_apub_lib::{
data::Data,
traits::{ActivityFields, ActivityHandler, ActorType},
- values::PublicUrl,
};
use lemmy_db_schema::{
source::community::{CommunityPersonBan, CommunityPersonBanForm},
#[serde(rename_all = "camelCase")]
pub struct UndoBlockUserFromCommunity {
actor: ObjectId<ApubPerson>,
- to: [PublicUrl; 1],
+ to: PublicUrlMigration,
object: BlockUserFromCommunity,
cc: [ObjectId<ApubCommunity>; 1],
#[serde(rename = "type")]
)?;
let undo = UndoBlockUserFromCommunity {
actor: ObjectId::new(actor.actor_id()),
- to: [PublicUrl::Public],
+ to: PublicUrlMigration::create(),
object: block,
cc: [ObjectId::new(community.actor_id())],
kind: UndoType::Undo,
},
context::lemmy_context,
fetcher::object_id::ObjectId,
+ migrations::PublicUrlMigration,
objects::{
community::{ApubCommunity, Group},
person::ApubPerson,
use lemmy_apub_lib::{
data::Data,
traits::{ActivityFields, ActivityHandler, ActorType, ToApub},
- values::PublicUrl,
};
use lemmy_db_schema::{
source::community::{Community, CommunityForm},
#[serde(rename_all = "camelCase")]
pub struct UpdateCommunity {
actor: ObjectId<ApubPerson>,
- to: [PublicUrl; 1],
+ to: PublicUrlMigration,
// TODO: would be nice to use a separate struct here, which only contains the fields updated here
object: Group,
cc: [ObjectId<ApubCommunity>; 1],
)?;
let update = UpdateCommunity {
actor: ObjectId::new(actor.actor_id()),
- to: [PublicUrl::Public],
+ to: PublicUrlMigration::create(),
object: community.to_apub(context.pool()).await?,
cc: [ObjectId::new(community.actor_id())],
kind: UpdateType::Update,
},
context::lemmy_context,
fetcher::object_id::ObjectId,
+ migrations::PublicUrlMigration,
objects::{community::ApubCommunity, person::ApubPerson},
};
use activitystreams::{
use lemmy_apub_lib::{
data::Data,
traits::{ActivityFields, ActivityHandler, ActorType},
- values::PublicUrl,
};
use lemmy_db_schema::{
source::{
#[serde(rename_all = "camelCase")]
pub struct Delete {
actor: ObjectId<ApubPerson>,
- to: [PublicUrl; 1],
+ to: PublicUrlMigration,
pub(in crate::activities::deletion) object: Url,
pub(in crate::activities::deletion) cc: [ObjectId<ApubCommunity>; 1],
#[serde(rename = "type")]
) -> Result<Delete, LemmyError> {
Ok(Delete {
actor: ObjectId::new(actor.actor_id()),
- to: [PublicUrl::Public],
+ to: PublicUrlMigration::create(),
object: object_id,
cc: [ObjectId::new(community.actor_id())],
kind: DeleteType::Delete,
},
context::lemmy_context,
fetcher::object_id::ObjectId,
+ migrations::PublicUrlMigration,
objects::{community::ApubCommunity, person::ApubPerson},
};
use activitystreams::{
use lemmy_apub_lib::{
data::Data,
traits::{ActivityFields, ActivityHandler, ActorType},
- values::PublicUrl,
};
use lemmy_db_schema::source::{comment::Comment, community::Community, post::Post};
use lemmy_utils::LemmyError;
#[serde(rename_all = "camelCase")]
pub struct UndoDelete {
actor: ObjectId<ApubPerson>,
- to: [PublicUrl; 1],
+ to: PublicUrlMigration,
object: Delete,
cc: [ObjectId<ApubCommunity>; 1],
#[serde(rename = "type")]
)?;
let undo = UndoDelete {
actor: ObjectId::new(actor.actor_id()),
- to: [PublicUrl::Public],
+ to: PublicUrlMigration::create(),
object,
cc: [ObjectId::new(community.actor_id())],
kind: UndoType::Undo,
generate_moderators_url,
objects::{community::ApubCommunity, person::ApubPerson},
};
+use activitystreams::public;
use anyhow::anyhow;
use lemmy_api_common::blocking;
use lemmy_apub_lib::{traits::ActivityFields, verify::verify_domains_match};
Ok(())
}
+pub(crate) fn verify_is_public(to: &[Url]) -> Result<(), LemmyError> {
+ if !to.contains(&public()) {
+ return Err(anyhow!("Object is not public").into());
+ }
+ Ok(())
+}
+
pub(crate) fn check_community_deleted_or_removed(community: &Community) -> Result<(), LemmyError> {
if community.deleted || community.removed {
Err(anyhow!("New post or comment cannot be created in deleted or removed community").into())
},
context::lemmy_context,
fetcher::object_id::ObjectId,
+ migrations::PublicUrlMigration,
objects::{
community::ApubCommunity,
person::ApubPerson,
use lemmy_apub_lib::{
data::Data,
traits::{ActivityFields, ActivityHandler, ActorType, FromApub, ToApub},
- values::PublicUrl,
verify::{verify_domains_match, verify_urls_match},
};
use lemmy_db_schema::{source::community::Community, traits::Crud};
#[serde(rename_all = "camelCase")]
pub struct CreateOrUpdatePost {
actor: ObjectId<ApubPerson>,
- to: [PublicUrl; 1],
+ to: PublicUrlMigration,
object: Page,
cc: [ObjectId<ApubCommunity>; 1],
#[serde(rename = "type")]
)?;
let create_or_update = CreateOrUpdatePost {
actor: ObjectId::new(actor.actor_id()),
- to: [PublicUrl::Public],
+ to: PublicUrlMigration::create(),
object: post.to_apub(context.pool()).await?,
cc: [ObjectId::new(community.actor_id())],
kind,
verify_activity,
},
fetcher::object_id::ObjectId,
+ migrations::PublicUrlMigration,
objects::{community::ApubCommunity, person::ApubPerson},
};
use activitystreams::{
use lemmy_apub_lib::{
data::Data,
traits::{ActivityFields, ActivityHandler},
- values::PublicUrl,
};
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
#[serde(rename_all = "camelCase")]
pub struct UndoRemovePostCommentOrCommunity {
actor: ObjectId<ApubPerson>,
- to: [PublicUrl; 1],
+ to: PublicUrlMigration,
// Note, there is no such thing as Undo/Remove/Mod, so we ignore that
object: RemoveMod,
cc: [ObjectId<ApubCommunity>; 1],
},
context::lemmy_context,
fetcher::object_id::ObjectId,
+ migrations::PublicUrlMigration,
objects::{community::ApubCommunity, person::ApubPerson},
PostOrComment,
};
use lemmy_apub_lib::{
data::Data,
traits::{ActivityFields, ActivityHandler, ActorType},
- values::PublicUrl,
verify::verify_urls_match,
};
use lemmy_db_schema::{newtypes::CommunityId, source::community::Community, traits::Crud};
#[serde(rename_all = "camelCase")]
pub struct UndoVote {
actor: ObjectId<ApubPerson>,
- to: [PublicUrl; 1],
+ to: PublicUrlMigration,
object: Vote,
cc: [ObjectId<ApubCommunity>; 1],
#[serde(rename = "type")]
)?;
let undo_vote = UndoVote {
actor: ObjectId::new(actor.actor_id()),
- to: [PublicUrl::Public],
+ to: PublicUrlMigration::create(),
object,
cc: [ObjectId::new(community.actor_id())],
kind: UndoType::Undo,
},
context::lemmy_context,
fetcher::object_id::ObjectId,
+ migrations::PublicUrlMigration,
objects::{community::ApubCommunity, person::ApubPerson},
PostOrComment,
};
use lemmy_apub_lib::{
data::Data,
traits::{ActivityFields, ActivityHandler, ActorType},
- values::PublicUrl,
};
use lemmy_db_schema::{newtypes::CommunityId, source::community::Community, traits::Crud};
use lemmy_utils::LemmyError;
#[serde(rename_all = "camelCase")]
pub struct Vote {
actor: ObjectId<ApubPerson>,
- to: [PublicUrl; 1],
+ to: PublicUrlMigration,
pub(in crate::activities::voting) object: ObjectId<PostOrComment>,
cc: [ObjectId<ApubCommunity>; 1],
#[serde(rename = "type")]
) -> Result<Vote, LemmyError> {
Ok(Vote {
actor: ObjectId::new(actor.actor_id()),
- to: [PublicUrl::Public],
+ to: PublicUrlMigration::create(),
object: ObjectId::new(object.ap_id()),
cc: [ObjectId::new(community.actor_id())],
kind: kind.clone(),
fetcher::{fetch::fetch_remote_object, object_id::ObjectId},
objects::{community::Group, person::ApubPerson},
};
-use activitystreams::collection::{CollectionExt, OrderedCollection};
+use activitystreams::{
+ base::AnyBase,
+ collection::{CollectionExt, OrderedCollection},
+};
use anyhow::Context;
use lemmy_api_common::blocking;
use lemmy_apub_lib::{data::Data, traits::ActivityHandler};
outbox_activities = outbox_activities[0..20].to_vec();
}
- for announce in outbox_activities {
- // TODO: instead of converting like this, we should create a struct CommunityOutbox with
- // AnnounceActivity as inner type, but that gives me stackoverflow
- let ser = serde_json::to_string(&announce)?;
- let announce: AnnounceActivity = serde_json::from_str(&ser)?;
- announce
- .receive(&Data::new(context.clone()), recursion_counter)
- .await?;
+ // We intentionally ignore errors here. This is because the outbox might contain posts from old
+ // Lemmy versions, or from other software which we cant parse. In that case, we simply skip the
+ // item and only parse the ones that work.
+ for activity in outbox_activities {
+ parse_outbox_item(activity, context, recursion_counter)
+ .await
+ .ok();
}
Ok(())
}
+async fn parse_outbox_item(
+ announce: AnyBase,
+ context: &LemmyContext,
+ request_counter: &mut i32,
+) -> Result<(), LemmyError> {
+ // TODO: instead of converting like this, we should create a struct CommunityOutbox with
+ // AnnounceActivity as inner type, but that gives me stackoverflow
+ let ser = serde_json::to_string(&announce)?;
+ let announce: AnnounceActivity = serde_json::from_str(&ser)?;
+ announce
+ .receive(&Data::new(context.clone()), request_counter)
+ .await?;
+ Ok(())
+}
+
async fn fetch_community_mods(
context: &LemmyContext,
group: &Group,
static REQUEST_LIMIT: i32 = 25;
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
+#[serde(transparent)]
pub struct ObjectId<Kind>(Url, #[serde(skip)] PhantomData<Kind>)
where
Kind: FromApub<DataType = LemmyContext> + ApubObject<DataType = LemmyContext> + Send + 'static,
use crate::fetcher::{object_id::ObjectId, post_or_comment::PostOrComment};
+use lemmy_apub_lib::values::PublicUrl;
use serde::{Deserialize, Serialize};
use url::Url;
//
// For v0.13, delete [`UndoRemovePostCommentOrCommunity`], and don't handle object deletion in
// [`RemoveMod`] handler.
+
+/// Migrate value of field `to` from single value to vec.
+///
+/// v0.14: send as single value, accept both
+/// v0.15: send as vec, accept both
+/// v0.16: send and accept only vec
+#[derive(Serialize, Deserialize, Debug, Clone)]
+#[serde(untagged)]
+pub enum PublicUrlMigration {
+ Old(PublicUrl),
+ New([PublicUrl; 1]),
+}
+
+impl PublicUrlMigration {
+ pub(crate) fn create() -> PublicUrlMigration {
+ PublicUrlMigration::Old(PublicUrl::Public)
+ }
+}
use crate::{
- activities::verify_person_in_community,
+ activities::{verify_is_public, verify_person_in_community},
context::lemmy_context,
fetcher::object_id::ObjectId,
migrations::CommentInReplyToMigration,
chrono::NaiveDateTime,
object::{kind::NoteType, Tombstone},
primitives::OneOrMany,
+ public,
unparsed::Unparsed,
};
use anyhow::{anyhow, Context};
use chrono::{DateTime, FixedOffset};
+use html2md::parse_html;
use lemmy_api_common::blocking;
use lemmy_apub_lib::{
traits::{ApubObject, FromApub, ToApub},
- values::{MediaTypeHtml, MediaTypeMarkdown, PublicUrl},
+ values::{MediaTypeHtml, MediaTypeMarkdown},
verify::verify_domains_match,
};
use lemmy_db_schema::{
/// Indicates that the object is publicly readable. Unlike [`Post.to`], this one doesn't contain
/// the community ID, as it would be incompatible with Pleroma (and we can get the community from
/// the post in [`in_reply_to`]).
- to: PublicUrl,
+ to: Vec<Url>,
content: String,
- media_type: MediaTypeHtml,
- source: Source,
+ media_type: Option<MediaTypeHtml>,
+ source: SourceCompat,
in_reply_to: CommentInReplyToMigration,
- published: DateTime<FixedOffset>,
+ published: Option<DateTime<FixedOffset>>,
updated: Option<DateTime<FixedOffset>>,
#[serde(flatten)]
unparsed: Unparsed,
}
+/// Pleroma puts a raw string in the source, so we have to handle it here for deserialization to work
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+#[serde(untagged)]
+enum SourceCompat {
+ Lemmy(Source),
+ Pleroma(String),
+}
+
impl Note {
pub(crate) fn id_unchecked(&self) -> &Url {
&self.id
request_counter,
)
.await?;
+ verify_is_public(&self.to)?;
Ok(())
}
}
r#type: NoteType::Note,
id: self.ap_id.to_owned().into_inner(),
attributed_to: ObjectId::new(creator.actor_id),
- to: PublicUrl::Public,
+ to: vec![public()],
content: self.content.clone(),
- media_type: MediaTypeHtml::Html,
- source: Source {
+ media_type: Some(MediaTypeHtml::Html),
+ source: SourceCompat::Lemmy(Source {
content: self.content.clone(),
media_type: MediaTypeMarkdown::Markdown,
- },
+ }),
in_reply_to: CommentInReplyToMigration::Old(in_reply_to_vec),
- published: convert_datetime(self.published),
+ published: Some(convert_datetime(self.published)),
updated: self.updated.map(convert_datetime),
unparsed: Default::default(),
};
return Err(anyhow!("Post is locked").into());
}
- let content = ¬e.source.content;
- let content_slurs_removed = remove_slurs(content, &context.settings().slur_regex());
+ let content = if let SourceCompat::Lemmy(source) = ¬e.source {
+ source.content.clone()
+ } else {
+ parse_html(¬e.content)
+ };
+ let content_slurs_removed = remove_slurs(&content, &context.settings().slur_regex());
let form = CommentForm {
creator_id: creator.id,
content: content_slurs_removed,
removed: None,
read: None,
- published: Some(note.published.naive_local()),
+ published: note.published.map(|u| u.to_owned().naive_local()),
updated: note.updated.map(|u| u.to_owned().naive_local()),
deleted: None,
ap_id,
Ok(comment.into())
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::objects::{
+ community::ApubCommunity,
+ tests::{file_to_json_object, init_context},
+ };
+ use assert_json_diff::assert_json_include;
+ use serial_test::serial;
+
+ async fn prepare_comment_test(
+ url: &Url,
+ context: &LemmyContext,
+ ) -> (ApubPerson, ApubCommunity, ApubPost) {
+ let person_json = file_to_json_object("assets/lemmy-person.json");
+ let person = ApubPerson::from_apub(&person_json, context, url, &mut 0)
+ .await
+ .unwrap();
+ let community_json = file_to_json_object("assets/lemmy-community.json");
+ let community = ApubCommunity::from_apub(&community_json, context, url, &mut 0)
+ .await
+ .unwrap();
+ let post_json = file_to_json_object("assets/lemmy-post.json");
+ let post = ApubPost::from_apub(&post_json, context, url, &mut 0)
+ .await
+ .unwrap();
+ (person, community, post)
+ }
+
+ fn cleanup(data: (ApubPerson, ApubCommunity, ApubPost), context: &LemmyContext) {
+ Post::delete(&*context.pool().get().unwrap(), data.2.id).unwrap();
+ Community::delete(&*context.pool().get().unwrap(), data.1.id).unwrap();
+ Person::delete(&*context.pool().get().unwrap(), data.0.id).unwrap();
+ }
+
+ #[actix_rt::test]
+ #[serial]
+ async fn test_fetch_lemmy_comment() {
+ let context = init_context();
+ let url = Url::parse("https://lemmy.ml/comment/38741").unwrap();
+ let data = prepare_comment_test(&url, &context).await;
+
+ let json = file_to_json_object("assets/lemmy-comment.json");
+ let mut request_counter = 0;
+ let comment = ApubComment::from_apub(&json, &context, &url, &mut request_counter)
+ .await
+ .unwrap();
+
+ assert_eq!(comment.ap_id.clone().into_inner(), url);
+ assert_eq!(comment.content.len(), 1063);
+ assert!(!comment.local);
+ assert_eq!(request_counter, 0);
+
+ let to_apub = comment.to_apub(context.pool()).await.unwrap();
+ assert_json_include!(actual: json, expected: to_apub);
+
+ Comment::delete(&*context.pool().get().unwrap(), comment.id).unwrap();
+ cleanup(data, &context);
+ }
+
+ #[actix_rt::test]
+ #[serial]
+ async fn test_fetch_pleroma_comment() {
+ let context = init_context();
+ let url = Url::parse("https://lemmy.ml/comment/38741").unwrap();
+ let data = prepare_comment_test(&url, &context).await;
+
+ let pleroma_url =
+ Url::parse("https://queer.hacktivis.me/objects/8d4973f4-53de-49cd-8c27-df160e16a9c2")
+ .unwrap();
+ let person_json = file_to_json_object("assets/pleroma-person.json");
+ ApubPerson::from_apub(&person_json, &context, &pleroma_url, &mut 0)
+ .await
+ .unwrap();
+ let json = file_to_json_object("assets/pleroma-comment.json");
+ let mut request_counter = 0;
+ let comment = ApubComment::from_apub(&json, &context, &pleroma_url, &mut request_counter)
+ .await
+ .unwrap();
+
+ assert_eq!(comment.ap_id.clone().into_inner(), pleroma_url);
+ assert_eq!(comment.content.len(), 64);
+ assert!(!comment.local);
+ assert_eq!(request_counter, 0);
+
+ Comment::delete(&*context.pool().get().unwrap(), comment.id).unwrap();
+ cleanup(data, &context);
+ }
+
+ #[actix_rt::test]
+ #[serial]
+ async fn test_html_to_markdown_sanitize() {
+ let parsed = parse_html("<script></script><b>hello</b>");
+ assert_eq!(parsed, "**hello**");
+ }
+}
LemmyError,
};
use lemmy_websocket::LemmyContext;
+use log::debug;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use std::ops::Deref;
followers: Url,
endpoints: Endpoints<Url>,
public_key: PublicKey,
- published: DateTime<FixedOffset>,
+ published: Option<DateTime<FixedOffset>>,
updated: Option<DateTime<FixedOffset>>,
#[serde(flatten)]
unparsed: Unparsed,
title,
description,
removed: None,
- published: Some(group.published.naive_local()),
+ published: group.published.map(|u| u.naive_local()),
updated: group.updated.map(|u| u.naive_local()),
deleted: None,
nsfw: Some(group.sensitive.unwrap_or(false)),
..Default::default()
},
public_key: self.get_public_key()?,
- published: convert_datetime(self.published),
+ published: Some(convert_datetime(self.published)),
updated: self.updated.map(convert_datetime),
unparsed: Default::default(),
};
) -> Result<ApubCommunity, LemmyError> {
let form = Group::from_apub_to_form(group, expected_domain, &context.settings()).await?;
+ // Fetching mods and outbox is not necessary for Lemmy to work, so ignore errors. Besides,
+ // we need to ignore these errors so that tests can work entirely offline.
let community = blocking(context.pool(), move |conn| Community::upsert(conn, &form)).await??;
- update_community_mods(group, &community, context, request_counter).await?;
+ update_community_mods(group, &community, context, request_counter)
+ .await
+ .map_err(|e| debug!("{}", e))
+ .ok();
// TODO: doing this unconditionally might cause infinite loop for some reason
- fetch_community_outbox(context, &group.outbox, request_counter).await?;
+ fetch_community_outbox(context, &group.outbox, request_counter)
+ .await
+ .map_err(|e| debug!("{}", e))
+ .ok();
Ok(community.into())
}
Ok(inboxes)
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::objects::tests::{file_to_json_object, init_context};
+ use assert_json_diff::assert_json_include;
+ use lemmy_db_schema::traits::Crud;
+ use serial_test::serial;
+
+ #[actix_rt::test]
+ #[serial]
+ async fn test_fetch_lemmy_community() {
+ let context = init_context();
+ let mut json: Group = file_to_json_object("assets/lemmy-community.json");
+ let json_orig = json.clone();
+ // change these links so they dont fetch over the network
+ json.moderators = Some(Url::parse("https://lemmy.ml/c/announcements/not_moderators").unwrap());
+ json.outbox = Url::parse("https://lemmy.ml/c/announcements/not_outbox").unwrap();
+
+ let url = Url::parse("https://lemmy.ml/c/announcements").unwrap();
+ let mut request_counter = 0;
+ let community = ApubCommunity::from_apub(&json, &context, &url, &mut request_counter)
+ .await
+ .unwrap();
+
+ assert_eq!(community.actor_id.clone().into_inner(), url);
+ assert_eq!(community.title, "Announcements");
+ assert!(community.public_key.is_some());
+ assert!(!community.local);
+ assert_eq!(community.description.as_ref().unwrap().len(), 126);
+ // this makes two requests to the (intentionally) broken outbox/moderators collections
+ assert_eq!(request_counter, 2);
+
+ let to_apub = community.to_apub(context.pool()).await.unwrap();
+ assert_json_include!(actual: json_orig, expected: to_apub);
+
+ Community::delete(&*context.pool().get().unwrap(), community.id).unwrap();
+ }
+}
Err(anyhow!("Cant convert object to tombstone if it wasnt deleted").into())
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use actix::Actor;
+ use diesel::{
+ r2d2::{ConnectionManager, Pool},
+ PgConnection,
+ };
+ use lemmy_apub_lib::activity_queue::create_activity_queue;
+ use lemmy_db_schema::{
+ establish_unpooled_connection,
+ get_database_url_from_env,
+ source::secret::Secret,
+ };
+ use lemmy_utils::{
+ rate_limit::{rate_limiter::RateLimiter, RateLimit},
+ request::build_user_agent,
+ settings::structs::Settings,
+ };
+ use lemmy_websocket::{chat_server::ChatServer, LemmyContext};
+ use reqwest::Client;
+ use serde::de::DeserializeOwned;
+ use std::{fs::File, io::BufReader, sync::Arc};
+ use tokio::sync::Mutex;
+
+ // TODO: would be nice if we didnt have to use a full context for tests.
+ // or at least write a helper function so this code is shared with main.rs
+ pub(crate) fn init_context() -> LemmyContext {
+ // call this to run migrations
+ establish_unpooled_connection();
+ let settings = Settings::init().unwrap();
+ let rate_limiter = RateLimit {
+ rate_limiter: Arc::new(Mutex::new(RateLimiter::default())),
+ rate_limit_config: settings.rate_limit.to_owned().unwrap_or_default(),
+ };
+ let client = Client::builder()
+ .user_agent(build_user_agent(&settings))
+ .build()
+ .unwrap();
+ let activity_queue = create_activity_queue();
+ let secret = Secret {
+ id: 0,
+ jwt_secret: "".to_string(),
+ };
+ let db_url = match get_database_url_from_env() {
+ Ok(url) => url,
+ Err(_) => settings.get_database_url(),
+ };
+ let manager = ConnectionManager::<PgConnection>::new(&db_url);
+ let pool = Pool::builder()
+ .max_size(settings.database.pool_size)
+ .build(manager)
+ .unwrap_or_else(|_| panic!("Error connecting to {}", db_url));
+ async fn x() -> Result<String, LemmyError> {
+ Ok("".to_string())
+ }
+ let chat_server = ChatServer::startup(
+ pool.clone(),
+ rate_limiter,
+ |_, _, _, _| Box::pin(x()),
+ |_, _, _, _| Box::pin(x()),
+ client.clone(),
+ activity_queue.clone(),
+ settings.clone(),
+ secret.clone(),
+ )
+ .start();
+ LemmyContext::create(pool, chat_server, client, activity_queue, settings, secret)
+ }
+
+ pub(crate) fn file_to_json_object<T: DeserializeOwned>(path: &str) -> T {
+ let file = File::open(path).unwrap();
+ let reader = BufReader::new(file);
+ serde_json::from_reader(reader).unwrap()
+ }
+}
outbox: Url,
endpoints: Endpoints<Url>,
public_key: PublicKey,
- published: DateTime<FixedOffset>,
+ published: Option<DateTime<FixedOffset>>,
updated: Option<DateTime<FixedOffset>>,
#[serde(flatten)]
unparsed: Unparsed,
icon,
image,
matrix_user_id: self.matrix_user_id.clone(),
- published: convert_datetime(self.published),
+ published: Some(convert_datetime(self.published)),
outbox: generate_outbox_url(&self.actor_id)?.into(),
endpoints: Endpoints {
shared_inbox: self.shared_inbox_url.clone().map(|s| s.into()),
deleted: None,
avatar: Some(person.icon.clone().map(|i| i.url.into())),
banner: Some(person.image.clone().map(|i| i.url.into())),
- published: Some(person.published.naive_local()),
+ published: person.published.map(|u| u.clone().naive_local()),
updated: person.updated.map(|u| u.clone().naive_local()),
actor_id,
bio: Some(bio),
Ok(person.into())
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::objects::tests::{file_to_json_object, init_context};
+ use assert_json_diff::assert_json_include;
+ use lemmy_db_schema::traits::Crud;
+ use serial_test::serial;
+
+ #[actix_rt::test]
+ #[serial]
+ async fn test_fetch_lemmy_person() {
+ let context = init_context();
+ let json = file_to_json_object("assets/lemmy-person.json");
+ let url = Url::parse("https://lemmy.ml/u/nutomic").unwrap();
+ let mut request_counter = 0;
+ let person = ApubPerson::from_apub(&json, &context, &url, &mut request_counter)
+ .await
+ .unwrap();
+
+ assert_eq!(person.actor_id.clone().into_inner(), url);
+ assert_eq!(person.name, "nutomic");
+ assert!(person.public_key.is_some());
+ assert!(!person.local);
+ assert!(person.bio.is_some());
+ assert_eq!(request_counter, 0);
+
+ let to_apub = person.to_apub(context.pool()).await.unwrap();
+ assert_json_include!(actual: json, expected: to_apub);
+
+ DbPerson::delete(&*context.pool().get().unwrap(), person.id).unwrap();
+ }
+
+ #[actix_rt::test]
+ #[serial]
+ async fn test_fetch_pleroma_person() {
+ let context = init_context();
+ let json = file_to_json_object("assets/pleroma-person.json");
+ let url = Url::parse("https://queer.hacktivis.me/users/lanodan").unwrap();
+ let mut request_counter = 0;
+ let person = ApubPerson::from_apub(&json, &context, &url, &mut request_counter)
+ .await
+ .unwrap();
+
+ assert_eq!(person.actor_id.clone().into_inner(), url);
+ assert_eq!(person.name, "lanodan");
+ assert!(person.public_key.is_some());
+ assert!(!person.local);
+ assert_eq!(request_counter, 0);
+ // TODO: pleroma uses summary for user profile, while we use content
+ //assert!(person.bio.is_some());
+
+ DbPerson::delete(&*context.pool().get().unwrap(), person.id).unwrap();
+ }
+}
use crate::{
- activities::{extract_community, verify_person_in_community},
+ activities::{extract_community, verify_is_public, verify_person_in_community},
context::lemmy_context,
fetcher::object_id::ObjectId,
objects::{create_tombstone, person::ApubPerson, ImageObject, Source},
r#type: PageType,
id: Url,
pub(crate) attributed_to: ObjectId<ApubPerson>,
- to: [Url; 2],
+ to: Vec<Url>,
name: String,
content: Option<String>,
- media_type: MediaTypeHtml,
+ media_type: Option<MediaTypeHtml>,
source: Option<Source>,
url: Option<Url>,
image: Option<ImageObject>,
pub(crate) comments_enabled: Option<bool>,
sensitive: Option<bool>,
pub(crate) stickied: Option<bool>,
- published: DateTime<FixedOffset>,
+ published: Option<DateTime<FixedOffset>>,
updated: Option<DateTime<FixedOffset>>,
#[serde(flatten)]
unparsed: Unparsed,
request_counter,
)
.await?;
+ verify_is_public(&self.to.clone())?;
Ok(())
}
}
r#type: PageType::Page,
id: self.ap_id.clone().into(),
attributed_to: ObjectId::new(creator.actor_id),
- to: [community.actor_id.into(), public()],
+ to: vec![community.actor_id.into(), public()],
name: self.name.clone(),
content: self.body.as_ref().map(|b| markdown_to_html(b)),
- media_type: MediaTypeHtml::Html,
+ media_type: Some(MediaTypeHtml::Html),
source,
url: self.url.clone().map(|u| u.into()),
image,
comments_enabled: Some(!self.locked),
sensitive: Some(self.nsfw),
stickied: Some(self.stickied),
- published: convert_datetime(self.published),
+ published: Some(convert_datetime(self.published)),
updated: self.updated.map(convert_datetime),
unparsed: Default::default(),
};
community_id: community.id,
removed: None,
locked: page.comments_enabled.map(|e| !e),
- published: Some(page.published.naive_local()),
+ published: page.published.map(|u| u.naive_local()),
updated: page.updated.map(|u| u.naive_local()),
deleted: None,
nsfw: page.sensitive,
Ok(post.into())
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::objects::{
+ community::ApubCommunity,
+ tests::{file_to_json_object, init_context},
+ };
+ use assert_json_diff::assert_json_include;
+ use serial_test::serial;
+
+ #[actix_rt::test]
+ #[serial]
+ async fn test_fetch_lemmy_post() {
+ let context = init_context();
+ let url = Url::parse("https://lemmy.ml/post/55143").unwrap();
+ let community_json = file_to_json_object("assets/lemmy-community.json");
+ let community = ApubCommunity::from_apub(&community_json, &context, &url, &mut 0)
+ .await
+ .unwrap();
+ let person_json = file_to_json_object("assets/lemmy-person.json");
+ let person = ApubPerson::from_apub(&person_json, &context, &url, &mut 0)
+ .await
+ .unwrap();
+ let json = file_to_json_object("assets/lemmy-post.json");
+ let mut request_counter = 0;
+ let post = ApubPost::from_apub(&json, &context, &url, &mut request_counter)
+ .await
+ .unwrap();
+
+ assert_eq!(post.ap_id.clone().into_inner(), url);
+ assert_eq!(post.name, "Statement on Politics of Lemmy.ml");
+ assert!(post.body.is_some());
+ assert_eq!(post.body.as_ref().unwrap().len(), 2144);
+ assert!(!post.locked);
+ assert!(post.stickied);
+ assert_eq!(request_counter, 0);
+
+ let to_apub = post.to_apub(context.pool()).await.unwrap();
+ assert_json_include!(actual: json, expected: to_apub);
+
+ Post::delete(&*context.pool().get().unwrap(), post.id).unwrap();
+ Person::delete(&*context.pool().get().unwrap(), person.id).unwrap();
+ Community::delete(&*context.pool().get().unwrap(), community.id).unwrap();
+ }
+}
content: String,
media_type: MediaTypeHtml,
source: Source,
- published: DateTime<FixedOffset>,
+ published: Option<DateTime<FixedOffset>>,
updated: Option<DateTime<FixedOffset>>,
#[serde(flatten)]
unparsed: Unparsed,
content: self.content.clone(),
media_type: MediaTypeMarkdown::Markdown,
},
- published: convert_datetime(self.published),
+ published: Some(convert_datetime(self.published)),
updated: self.updated.map(convert_datetime),
unparsed: Default::default(),
};
creator_id: creator.id,
recipient_id: recipient.id,
content: note.source.content.clone(),
- published: Some(note.published.naive_local()),
+ published: note.published.map(|u| u.to_owned().naive_local()),
updated: note.updated.map(|u| u.to_owned().naive_local()),
deleted: None,
read: None,
Ok(pm.into())
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::objects::tests::{file_to_json_object, init_context};
+ use assert_json_diff::assert_json_include;
+ use serial_test::serial;
+
+ #[actix_rt::test]
+ #[serial]
+ async fn test_fetch_lemmy_pm() {
+ let context = init_context();
+ let url = Url::parse("https://lemmy.ml/private_message/1621").unwrap();
+ let lemmy_person = file_to_json_object("assets/lemmy-person.json");
+ let person1 = ApubPerson::from_apub(&lemmy_person, &context, &url, &mut 0)
+ .await
+ .unwrap();
+ let pleroma_person = file_to_json_object("assets/pleroma-person.json");
+ let pleroma_url = Url::parse("https://queer.hacktivis.me/users/lanodan").unwrap();
+ let person2 = ApubPerson::from_apub(&pleroma_person, &context, &pleroma_url, &mut 0)
+ .await
+ .unwrap();
+ let json = file_to_json_object("assets/lemmy-private-message.json");
+ let mut request_counter = 0;
+ let pm = ApubPrivateMessage::from_apub(&json, &context, &url, &mut request_counter)
+ .await
+ .unwrap();
+
+ assert_eq!(pm.ap_id.clone().into_inner(), url);
+ assert_eq!(pm.content.len(), 4);
+ assert_eq!(request_counter, 0);
+
+ let to_apub = pm.to_apub(context.pool()).await.unwrap();
+ assert_json_include!(actual: json, expected: to_apub);
+
+ PrivateMessage::delete(&*context.pool().get().unwrap(), pm.id).unwrap();
+ Person::delete(&*context.pool().get().unwrap(), person1.id).unwrap();
+ Person::delete(&*context.pool().get().unwrap(), person2.id).unwrap();
+ }
+}
.set(private_message_form)
.get_result::<Self>(conn)
}
+ fn delete(conn: &PgConnection, pm_id: Self::IdType) -> Result<usize, Error> {
+ use crate::schema::private_message::dsl::*;
+ diesel::delete(private_message.find(pm_id)).execute(conn)
+ }
}
impl PrivateMessage {
pub mod send;
pub struct LemmyContext {
- pub pool: DbPool,
- pub chat_server: Addr<ChatServer>,
- pub client: Client,
- pub activity_queue: QueueHandle,
- pub settings: Settings,
- pub secret: Secret,
+ pool: DbPool,
+ chat_server: Addr<ChatServer>,
+ client: Client,
+ activity_queue: QueueHandle,
+ settings: Settings,
+ secret: Secret,
}
impl LemmyContext {