diff --git a/Dockerfile b/Dockerfile index 9a225af5b..67da44438 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:buster-slim +FROM debian:bullseye-slim ENV DOMAIN=localhost RUN apt-get update && \ apt-get -y install \ diff --git a/Makefile b/Makefile index 7a54fd9db..2f4863a5e 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,8 @@ all: debug: source: rm -f *.*~ *~ + rm -f ontology/*~ + rm -f ontology/*.new rm -f translations/*~ rm -f orgs/*~ rm -f scripts/*~ @@ -17,6 +19,8 @@ source: clean: rm -f *.*~ *~ *.dot rm -f orgs/*~ + rm -f ontology/*~ + rm -f ontology/*.new rm -f defaultwelcome/*~ rm -f theme/indymediaclassic/welcome/*~ rm -f theme/indymediamodern/welcome/*~ diff --git a/README.md b/README.md index 27d01c317..f6b9079ba 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ Add issues on https://gitlab.com/bashrc2/epicyon/-/issues
Epicyon, meaning "more than a dog". Largest of the Borophaginae which lived in North America 20-5 million years ago.
- + - + Epicyon is a modern [ActivityPub](https://www.w3.org/TR/activitypub) compliant server implementing both S2S and C2S protocols and suitable for installation on single board computers. It includes features such as moderation tools, post expiry, content warnings, image descriptions, news feed and perimeter defense against adversaries. It contains *no JavaScript* and uses HTML+CSS with a Python backend. @@ -16,9 +16,9 @@ Matrix room: **#epicyon:matrix.freedombone.net** Includes emojis designed by [OpenMoji](https://openmoji.org) – the open-source emoji and icon project. License: [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0). Blob Cat Emoji and Meowmoji were made by Nitro Blob Hub, licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0). [Digital Pets emoji](https://opengameart.org/content/16x16-emotes-for-rpgs-and-digital-pets) were made by Tomcat94 and licensed under CC0. - + - + ## Package Dependencies diff --git a/README_goals.md b/README_goals.md index 8403bba5c..531bc467d 100644 --- a/README_goals.md +++ b/README_goals.md @@ -34,19 +34,23 @@ * Integration with RSS feeds, for reading news or blogs * Moderation capabilities for posts, hashtags and blocks -**Features which won't be implemented** +## Non-goals The following are considered anti-features of other social network systems, since they encourage dysfunctional social interactions. * Features designed to scale to large numbers of accounts (say, more than 20 active users) * Trending hashtags, or trending anything * Ranking, rating or recommending mechanisms for posts or people (other than likes or repeats/boosts) - * Geo-location features + * Geo-location features, unless they're always opt-in * Algorithmic timelines (i.e. non-chronological) * Direct payment mechanisms, although integration with other services may be possible * Any variety of blockchain + * Anything based upon "proof of stake". The "people who have more, get more" principle should be rejected. + * Like counts above some small maximum number. The aim is to avoid people getting addicted to making numbers go up, and especially to avoid the dark market in fake likes. * Sponsored posts * Enterprise features for use cases applicable only to businesses. Epicyon could be used in a small business, but it's not primarily designed for that * Collaborative editing of posts, although you could do that outside of this system using Etherpad, or similar * Anonymous posts from random internet users published under a single generic instance account - * Hierarchies of roles beyond ordinary moderation, such as X requires special agreement from Y before sending a post + * Hierarchies of roles beyond ordinary moderation, such as X requires special agreement from Y before sending a post. Originally delegated roles were envisioned, but later abandoned due to the potential for creating elaborate hierarchies + * Federated blocklists. Initially this seems like a good idea, but the potential down sides outweigh the benefits. eg. Two allied instances share their global blocklist. Some time later one instance is transferred to an adversary, or gets hacked or sold. Adversary can now control your global blocklist and trash your instance very quickly that way. + * Federated moderation. Again, seems like it might be beneficial initially. Share the burden of moderation. But under realistic conditions people could be pressured or bribed into giving federated moderation access, and the consequences could be very bad. Individuals going on power trips, controlling multiple instances and heading back towards centralization. Avoid creating technical routes which easily lead to power consolidation and centralization. diff --git a/README_roadmap.md b/README_roadmap.md index bfc0ddc4f..b1a6eb428 100644 --- a/README_roadmap.md +++ b/README_roadmap.md @@ -6,8 +6,9 @@ ## Groups - * Unit test for group creation * Groups can be defined as having particular roles/skills + * Parse posts from Lemmy groups + * Think of a way to display groups. Maybe assign a hashtag and display them like hashtag timelines ## Questions @@ -19,6 +20,7 @@ ## Code * More unit test coverage + * Unit test for federated shared items * Break up large functions into smaller ones * Architecture diagrams * Code documentation? diff --git a/acceptreject.py b/acceptreject.py index e61b6d558..d4d0e22ec 100644 --- a/acceptreject.py +++ b/acceptreject.py @@ -17,6 +17,8 @@ from utils import domainPermitted from utils import followPerson from utils import hasObjectDict from utils import acctDir +from utils import hasGroupType +from utils import localActorUrl def _createAcceptReject(baseDir: str, federationList: [], @@ -40,7 +42,7 @@ def _createAcceptReject(baseDir: str, federationList: [], newAccept = { "@context": "https://www.w3.org/ns/activitystreams", 'type': acceptType, - 'actor': httpPrefix + '://' + domain + '/users/' + nickname, + 'actor': localActorUrl(httpPrefix, nickname, domain), 'to': [toUrl], 'cc': [], 'object': objectJson @@ -160,10 +162,16 @@ def _acceptFollow(baseDir: str, domain: str, messageJson: {}, ' but they have been unfollowed') return + # does the url path indicate that this is a group actor + groupAccount = hasGroupType(baseDir, followedActor, None, debug) + if debug: + print('Accepted follow is a group: ' + str(groupAccount) + + ' ' + followedActor + ' ' + baseDir) + if followPerson(baseDir, nickname, acceptedDomainFull, followedNickname, followedDomainFull, - federationList, debug): + federationList, debug, groupAccount): if debug: print('DEBUG: ' + nickname + '@' + acceptedDomainFull + ' followed ' + followedNickname + '@' + followedDomainFull) diff --git a/announce.py b/announce.py index 92c412c5c..8fc49bf8d 100644 --- a/announce.py +++ b/announce.py @@ -7,6 +7,7 @@ __email__ = "bob@freedombone.net" __status__ = "Production" __module_group__ = "ActivityPub" +from utils import hasGroupType from utils import removeDomainPort from utils import hasObjectDict from utils import removeIdEnding @@ -21,6 +22,7 @@ from utils import locatePost from utils import saveJson from utils import undoAnnounceCollectionEntry from utils import updateAnnounceCollection +from utils import localActorUrl from posts import sendSignedJson from posts import getPersonBox from session import postJson @@ -135,11 +137,11 @@ def createAnnounce(session, baseDir: str, federationList: [], statusNumber, published = getStatusNumber() newAnnounceId = httpPrefix + '://' + fullDomain + \ '/users/' + nickname + '/statuses/' + statusNumber - atomUriStr = httpPrefix + '://' + fullDomain + '/users/' + nickname + \ + atomUriStr = localActorUrl(httpPrefix, nickname, fullDomain) + \ '/statuses/' + statusNumber newAnnounce = { "@context": "https://www.w3.org/ns/activitystreams", - 'actor': httpPrefix + '://' + fullDomain + '/users/' + nickname, + 'actor': localActorUrl(httpPrefix, nickname, fullDomain), 'atomUri': atomUriStr, 'cc': [], 'id': newAnnounceId + '/activity', @@ -159,9 +161,16 @@ def createAnnounce(session, baseDir: str, federationList: [], announceNickname = None announceDomain = None announcePort = None + groupAccount = False if hasUsersPath(objectUrl): announceNickname = getNicknameFromActor(objectUrl) announceDomain, announcePort = getDomainFromActor(objectUrl) + if '/' + str(announceNickname) + '/' in objectUrl: + announceActor = \ + objectUrl.split('/' + announceNickname + '/')[0] + \ + '/' + announceNickname + if hasGroupType(baseDir, announceActor, personCache): + groupAccount = True if announceNickname and announceDomain: sendSignedJson(newAnnounce, session, baseDir, @@ -169,7 +178,7 @@ def createAnnounce(session, baseDir: str, federationList: [], announceNickname, announceDomain, announcePort, None, httpPrefix, True, clientToServer, federationList, sendThreads, postLog, cachedWebfingers, personCache, - debug, projectVersion) + debug, projectVersion, None, groupAccount) return newAnnounce @@ -185,8 +194,7 @@ def announcePublic(session, baseDir: str, federationList: [], fromDomain = getFullDomain(domain, port) toUrl = 'https://www.w3.org/ns/activitystreams#Public' - ccUrl = httpPrefix + '://' + fromDomain + '/users/' + nickname + \ - '/followers' + ccUrl = localActorUrl(httpPrefix, nickname, fromDomain) + '/followers' return createAnnounce(session, baseDir, federationList, nickname, domain, port, toUrl, ccUrl, httpPrefix, @@ -211,13 +219,11 @@ def sendAnnounceViaServer(baseDir: str, session, fromDomainFull = getFullDomain(fromDomain, fromPort) toUrl = 'https://www.w3.org/ns/activitystreams#Public' - ccUrl = httpPrefix + '://' + fromDomainFull + '/users/' + fromNickname + \ - '/followers' + actorStr = localActorUrl(httpPrefix, fromNickname, fromDomainFull) + ccUrl = actorStr + '/followers' statusNumber, published = getStatusNumber() - newAnnounceId = httpPrefix + '://' + fromDomainFull + '/users/' + \ - fromNickname + '/statuses/' + statusNumber - actorStr = httpPrefix + '://' + fromDomainFull + '/users/' + fromNickname + newAnnounceId = actorStr + '/statuses/' + statusNumber newAnnounceJson = { "@context": "https://www.w3.org/ns/activitystreams", 'actor': actorStr, @@ -235,7 +241,7 @@ def sendAnnounceViaServer(baseDir: str, session, # lookup the inbox for the To handle wfRequest = webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - fromDomain, projectVersion, debug) + fromDomain, projectVersion, debug, False) if not wfRequest: if debug: print('DEBUG: announce webfinger failed for ' + handle) @@ -300,7 +306,7 @@ def sendUndoAnnounceViaServer(baseDir: str, session, domainFull = getFullDomain(domain, port) - actor = httpPrefix + '://' + domainFull + '/users/' + nickname + actor = localActorUrl(httpPrefix, nickname, domainFull) handle = actor.replace('/users/', '/@') statusNumber, published = getStatusNumber() @@ -315,7 +321,7 @@ def sendUndoAnnounceViaServer(baseDir: str, session, # lookup the inbox for the To handle wfRequest = webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - domain, projectVersion, debug) + domain, projectVersion, debug, False) if not wfRequest: if debug: print('DEBUG: undo announce webfinger failed for ' + handle) diff --git a/architecture/epicyon_groups_ActivityPub.png b/architecture/epicyon_groups_ActivityPub.png index d8928af1c..48615e578 100644 Binary files a/architecture/epicyon_groups_ActivityPub.png and b/architecture/epicyon_groups_ActivityPub.png differ diff --git a/architecture/epicyon_groups_ActivityPub_Core.png b/architecture/epicyon_groups_ActivityPub_Core.png index 4dfb426e5..e64e8938c 100644 Binary files a/architecture/epicyon_groups_ActivityPub_Core.png and b/architecture/epicyon_groups_ActivityPub_Core.png differ diff --git a/architecture/epicyon_groups_ActivityPub_Security.png b/architecture/epicyon_groups_ActivityPub_Security.png index c68653ec0..0e16a9626 100644 Binary files a/architecture/epicyon_groups_ActivityPub_Security.png and b/architecture/epicyon_groups_ActivityPub_Security.png differ diff --git a/architecture/epicyon_groups_Commandline-Interface_ActivityPub.png b/architecture/epicyon_groups_Commandline-Interface_ActivityPub.png index 5aabb8f9d..9a60266f4 100644 Binary files a/architecture/epicyon_groups_Commandline-Interface_ActivityPub.png and b/architecture/epicyon_groups_Commandline-Interface_ActivityPub.png differ diff --git a/architecture/epicyon_groups_Commandline-Interface_Core.png b/architecture/epicyon_groups_Commandline-Interface_Core.png index 5fdd5bd0e..99a1d22e1 100644 Binary files a/architecture/epicyon_groups_Commandline-Interface_Core.png and b/architecture/epicyon_groups_Commandline-Interface_Core.png differ diff --git a/architecture/epicyon_groups_Core.png b/architecture/epicyon_groups_Core.png index 5c4885c9f..5ea67e7aa 100644 Binary files a/architecture/epicyon_groups_Core.png and b/architecture/epicyon_groups_Core.png differ diff --git a/architecture/epicyon_groups_Core_Accessibility.png b/architecture/epicyon_groups_Core_Accessibility.png index 8a860e0f4..f8a2eba5e 100644 Binary files a/architecture/epicyon_groups_Core_Accessibility.png and b/architecture/epicyon_groups_Core_Accessibility.png differ diff --git a/architecture/epicyon_groups_Core_Security.png b/architecture/epicyon_groups_Core_Security.png index 5cdf37b91..70196e4b3 100644 Binary files a/architecture/epicyon_groups_Core_Security.png and b/architecture/epicyon_groups_Core_Security.png differ diff --git a/architecture/epicyon_groups_Timeline_Core.png b/architecture/epicyon_groups_Timeline_Core.png index 50f67dd04..434e99afd 100644 Binary files a/architecture/epicyon_groups_Timeline_Core.png and b/architecture/epicyon_groups_Timeline_Core.png differ diff --git a/architecture/epicyon_groups_Timeline_Security.png b/architecture/epicyon_groups_Timeline_Security.png index 4a27e079b..a7d60b4b1 100644 Binary files a/architecture/epicyon_groups_Timeline_Security.png and b/architecture/epicyon_groups_Timeline_Security.png differ diff --git a/architecture/epicyon_groups_Web-Interface-Columns_Core.png b/architecture/epicyon_groups_Web-Interface-Columns_Core.png index 4fc87f114..4c0b9628c 100644 Binary files a/architecture/epicyon_groups_Web-Interface-Columns_Core.png and b/architecture/epicyon_groups_Web-Interface-Columns_Core.png differ diff --git a/architecture/epicyon_groups_Web-Interface_Accessibility.png b/architecture/epicyon_groups_Web-Interface_Accessibility.png index 738fd1f73..69fdfe77f 100644 Binary files a/architecture/epicyon_groups_Web-Interface_Accessibility.png and b/architecture/epicyon_groups_Web-Interface_Accessibility.png differ diff --git a/architecture/epicyon_groups_Web-Interface_Core.png b/architecture/epicyon_groups_Web-Interface_Core.png index 49e98b3a2..281401663 100644 Binary files a/architecture/epicyon_groups_Web-Interface_Core.png and b/architecture/epicyon_groups_Web-Interface_Core.png differ diff --git a/auth.py b/auth.py index 5103365f3..de23e6130 100644 --- a/auth.py +++ b/auth.py @@ -89,7 +89,7 @@ def authorizeBasic(baseDir: str, path: str, authHeader: str, """ if ' ' not in authHeader: if debug: - print('DEBUG: basic auth - Authorixation header does not ' + + print('DEBUG: basic auth - Authorisation header does not ' + 'contain a space character') return False if not hasUsersPath(path): @@ -132,9 +132,10 @@ def authorizeBasic(baseDir: str, path: str, authHeader: str, print('DEBUG: passwords file missing') return False providedPassword = plain.split(':')[1] - passfile = open(passwordFile, 'r') - for line in passfile: - if line.startswith(nickname + ':'): + with open(passwordFile, 'r') as passfile: + for line in passfile: + if not line.startswith(nickname + ':'): + continue storedPassword = \ line.split(':')[1].replace('\n', '').replace('\r', '') success = _verifyPassword(storedPassword, providedPassword) diff --git a/availability.py b/availability.py index ce7f01d11..35ba9164e 100644 --- a/availability.py +++ b/availability.py @@ -18,6 +18,7 @@ from utils import getDomainFromActor from utils import loadJson from utils import saveJson from utils import acctDir +from utils import localActorUrl def setAvailability(baseDir: str, nickname: str, domain: str, @@ -90,13 +91,12 @@ def sendAvailabilityViaServer(baseDir: str, session, domainFull = getFullDomain(domain, port) - toUrl = httpPrefix + '://' + domainFull + '/users/' + nickname - ccUrl = httpPrefix + '://' + domainFull + '/users/' + nickname + \ - '/followers' + toUrl = localActorUrl(httpPrefix, nickname, domainFull) + ccUrl = toUrl + '/followers' newAvailabilityJson = { 'type': 'Availability', - 'actor': httpPrefix + '://' + domainFull + '/users/' + nickname, + 'actor': toUrl, 'object': '"' + status + '"', 'to': [toUrl], 'cc': [ccUrl] @@ -107,7 +107,7 @@ def sendAvailabilityViaServer(baseDir: str, session, # lookup the inbox for the To handle wfRequest = webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - domain, projectVersion, debug) + domain, projectVersion, debug, False) if not wfRequest: if debug: print('DEBUG: availability webfinger failed for ' + handle) diff --git a/blocking.py b/blocking.py index 6988bc310..406b79717 100644 --- a/blocking.py +++ b/blocking.py @@ -28,6 +28,9 @@ from utils import evilIncarnate from utils import getDomainFromActor from utils import getNicknameFromActor from utils import acctDir +from utils import localActorUrl +from conversation import muteConversation +from conversation import unmuteConversation def addGlobalBlock(baseDir: str, @@ -60,12 +63,39 @@ def addBlock(baseDir: str, nickname: str, domain: str, blockNickname: str, blockDomain: str) -> bool: """Block the given account """ + if blockDomain.startswith(domain) and nickname == blockNickname: + # don't block self + return False + domain = removeDomainPort(domain) blockingFilename = acctDir(baseDir, nickname, domain) + '/blocking.txt' blockHandle = blockNickname + '@' + blockDomain if os.path.isfile(blockingFilename): - if blockHandle in open(blockingFilename).read(): + if blockHandle + '\n' in open(blockingFilename).read(): return False + + # if we are following then unfollow + followingFilename = acctDir(baseDir, nickname, domain) + '/following.txt' + if os.path.isfile(followingFilename): + if blockHandle + '\n' in open(followingFilename).read(): + followingStr = '' + with open(followingFilename, 'r') as followingFile: + followingStr = followingFile.read() + followingStr = followingStr.replace(blockHandle + '\n', '') + with open(followingFilename, 'w+') as followingFile: + followingFile.write(followingStr) + + # if they are a follower then remove them + followersFilename = acctDir(baseDir, nickname, domain) + '/followers.txt' + if os.path.isfile(followersFilename): + if blockHandle + '\n' in open(followersFilename).read(): + followersStr = '' + with open(followersFilename, 'r') as followersFile: + followersStr = followersFile.read() + followersStr = followersStr.replace(blockHandle + '\n', '') + with open(followersFilename, 'w+') as followersFile: + followersFile.write(followersStr) + with open(blockingFilename, 'a+') as blockFile: blockFile.write(blockHandle + '\n') return True @@ -305,25 +335,25 @@ def isBlocked(baseDir: str, nickname: str, domain: str, def outboxBlock(baseDir: str, httpPrefix: str, nickname: str, domain: str, port: int, - messageJson: {}, debug: bool) -> None: + messageJson: {}, debug: bool) -> bool: """ When a block request is received by the outbox from c2s """ if not messageJson.get('type'): if debug: print('DEBUG: block - no type') - return + return False if not messageJson['type'] == 'Block': if debug: print('DEBUG: not a block') - return + return False if not messageJson.get('object'): if debug: print('DEBUG: no object in block') - return + return False if not isinstance(messageJson['object'], str): if debug: print('DEBUG: block object is not string') - return + return False if debug: print('DEBUG: c2s block request arrived in outbox') @@ -331,22 +361,22 @@ def outboxBlock(baseDir: str, httpPrefix: str, if '/statuses/' not in messageId: if debug: print('DEBUG: c2s block object is not a status') - return + return False if not hasUsersPath(messageId): if debug: print('DEBUG: c2s block object has no nickname') - return + return False domain = removeDomainPort(domain) postFilename = locatePost(baseDir, nickname, domain, messageId) if not postFilename: if debug: print('DEBUG: c2s block post not found in inbox or outbox') print(messageId) - return + return False nicknameBlocked = getNicknameFromActor(messageJson['object']) if not nicknameBlocked: print('WARN: unable to find nickname in ' + messageJson['object']) - return + return False domainBlocked, portBlocked = getDomainFromActor(messageJson['object']) domainBlockedFull = getFullDomain(domainBlocked, portBlocked) @@ -355,6 +385,7 @@ def outboxBlock(baseDir: str, httpPrefix: str, if debug: print('DEBUG: post blocked via c2s - ' + postFilename) + return True def outboxUndoBlock(baseDir: str, httpPrefix: str, @@ -439,7 +470,12 @@ def mutePost(baseDir: str, nickname: str, domain: str, port: int, if hasObjectDict(postJsonObject): domainFull = getFullDomain(domain, port) - actor = httpPrefix + '://' + domainFull + '/users/' + nickname + actor = localActorUrl(httpPrefix, nickname, domainFull) + + if postJsonObject['object'].get('conversation'): + muteConversation(baseDir, nickname, domain, + postJsonObject['object']['conversation']) + # does this post have ignores on it from differenent actors? if not postJsonObject['object'].get('ignores'): if debug: @@ -518,9 +554,13 @@ def unmutePost(baseDir: str, nickname: str, domain: str, port: int, print('UNMUTE: ' + muteFilename + ' file removed') if hasObjectDict(postJsonObject): + if postJsonObject['object'].get('conversation'): + unmuteConversation(baseDir, nickname, domain, + postJsonObject['object']['conversation']) + if postJsonObject['object'].get('ignores'): domainFull = getFullDomain(domain, port) - actor = httpPrefix + '://' + domainFull + '/users/' + nickname + actor = localActorUrl(httpPrefix, nickname, domainFull) totalItems = 0 if postJsonObject['object']['ignores'].get('totalItems'): totalItems = \ diff --git a/blog.py b/blog.py index 7823ae3b0..58ac947ca 100644 --- a/blog.py +++ b/blog.py @@ -15,7 +15,12 @@ from webapp_utils import htmlHeaderWithExternalStyle from webapp_utils import htmlHeaderWithBlogMarkup from webapp_utils import htmlFooter from webapp_utils import getPostAttachmentsAsHtml +from webapp_utils import editTextArea from webapp_media import addEmbeddedElements +from utils import localActorUrl +from utils import getActorLanguagesList +from utils import getBaseContentFromPost +from utils import getContentFromPost from utils import isAccountDir from utils import removeHtml from utils import getConfigParam @@ -31,6 +36,7 @@ from utils import acctDir from posts import createBlogsTimeline from newswire import rss2Header from newswire import rss2Footer +from cache import getPersonFromCache def _noOfBlogReplies(baseDir: str, httpPrefix: str, translate: {}, @@ -164,7 +170,9 @@ def _htmlBlogPostContent(authorized: bool, postJsonObject: {}, handle: str, restrictToDomain: bool, peertubeInstances: [], - blogSeparator='
') -> str: + systemLanguage: str, + personCache: {}, + blogSeparator: str = '
') -> str: """Returns the content for a single blog post """ linkedAuthor = False @@ -235,9 +243,16 @@ def _htmlBlogPostContent(authorized: bool, if attachmentStr: blogStr += '
' + attachmentStr + '
' - if postJsonObject['object'].get('content'): - contentStr = addEmbeddedElements(translate, - postJsonObject['object']['content'], + personUrl = localActorUrl(httpPrefix, nickname, domainFull) + actorJson = \ + getPersonFromCache(baseDir, personUrl, personCache, False) + languagesUnderstood = [] + if actorJson: + languagesUnderstood = getActorLanguagesList(actorJson) + jsonContent = getContentFromPost(postJsonObject, systemLanguage, + languagesUnderstood) + if jsonContent: + contentStr = addEmbeddedElements(translate, jsonContent, peertubeInstances) if postJsonObject['object'].get('tag'): contentStr = replaceEmojiFromTags(contentStr, @@ -273,8 +288,8 @@ def _htmlBlogPostContent(authorized: bool, if not linkedAuthor: blogStr += '

' + translate['About the author'] + \ + localActorUrl(httpPrefix, nickname, domainFull) + \ + '">' + translate['About the author'] + \ '

\n' replies = _noOfBlogReplies(baseDir, httpPrefix, translate, @@ -312,7 +327,8 @@ def _htmlBlogPostRSS2(authorized: bool, baseDir: str, httpPrefix: str, translate: {}, nickname: str, domain: str, domainFull: str, postJsonObject: {}, - handle: str, restrictToDomain: bool) -> str: + handle: str, restrictToDomain: bool, + systemLanguage: str) -> str: """Returns the RSS version 2 feed for a single blog post """ rssStr = '' @@ -327,7 +343,8 @@ def _htmlBlogPostRSS2(authorized: bool, pubDate = datetime.strptime(published, "%Y-%m-%dT%H:%M:%SZ") titleStr = postJsonObject['object']['summary'] rssDateStr = pubDate.strftime("%a, %d %b %Y %H:%M:%S UT") - content = postJsonObject['object']['content'] + content = \ + getBaseContentFromPost(postJsonObject, systemLanguage) description = firstParagraphFromString(content) rssStr = ' ' rssStr += ' ' + titleStr + '' @@ -343,7 +360,8 @@ def _htmlBlogPostRSS3(authorized: bool, baseDir: str, httpPrefix: str, translate: {}, nickname: str, domain: str, domainFull: str, postJsonObject: {}, - handle: str, restrictToDomain: bool) -> str: + handle: str, restrictToDomain: bool, + systemLanguage: str) -> str: """Returns the RSS version 3 feed for a single blog post """ rssStr = '' @@ -358,7 +376,8 @@ def _htmlBlogPostRSS3(authorized: bool, pubDate = datetime.strptime(published, "%Y-%m-%dT%H:%M:%SZ") titleStr = postJsonObject['object']['summary'] rssDateStr = pubDate.strftime("%a, %d %b %Y %H:%M:%S UT") - content = postJsonObject['object']['content'] + content = \ + getBaseContentFromPost(postJsonObject, systemLanguage) description = firstParagraphFromString(content) rssStr = 'title: ' + titleStr + '\n' rssStr += 'link: ' + messageLink + '\n' @@ -379,10 +398,10 @@ def _htmlBlogRemoveCwButton(blogStr: str, translate: {}) -> str: return blogStr -def _getSnippetFromBlogContent(postJsonObject: {}) -> str: +def _getSnippetFromBlogContent(postJsonObject: {}, systemLanguage: str) -> str: """Returns a snippet of text from the blog post as a preview """ - content = postJsonObject['object']['content'] + content = getBaseContentFromPost(postJsonObject, systemLanguage) if '

' in content: content = content.split('

', 1)[1] if '

' in content: @@ -400,7 +419,7 @@ def htmlBlogPost(authorized: bool, nickname: str, domain: str, domainFull: str, postJsonObject: {}, peertubeInstances: [], - systemLanguage: str) -> str: + systemLanguage: str, personCache: {}) -> str: """Returns a html blog post """ blogStr = '' @@ -412,7 +431,7 @@ def htmlBlogPost(authorized: bool, getConfigParam(baseDir, 'instanceTitle') published = postJsonObject['object']['published'] title = postJsonObject['object']['summary'] - snippet = _getSnippetFromBlogContent(postJsonObject) + snippet = _getSnippetFromBlogContent(postJsonObject, systemLanguage) blogStr = htmlHeaderWithBlogMarkup(cssFilename, instanceTitle, httpPrefix, domainFull, nickname, systemLanguage, published, @@ -424,7 +443,8 @@ def htmlBlogPost(authorized: bool, nickname, domain, domainFull, postJsonObject, None, False, - peertubeInstances) + peertubeInstances, systemLanguage, + personCache) # show rss links blogStr += '

' @@ -452,7 +472,8 @@ def htmlBlogPage(authorized: bool, session, baseDir: str, httpPrefix: str, translate: {}, nickname: str, domain: str, port: int, noOfItems: int, pageNumber: int, - peertubeInstances: []) -> str: + peertubeInstances: [], systemLanguage: str, + personCache: {}) -> str: """Returns a html blog page containing posts """ if ' ' in nickname or '@' in nickname or \ @@ -514,7 +535,9 @@ def htmlBlogPage(authorized: bool, session, nickname, domain, domainFull, item, None, True, - peertubeInstances) + peertubeInstances, + systemLanguage, + personCache) if len(timelineJson['orderedItems']) >= noOfItems: blogStr += navigateStr @@ -542,7 +565,7 @@ def htmlBlogPageRSS2(authorized: bool, session, baseDir: str, httpPrefix: str, translate: {}, nickname: str, domain: str, port: int, noOfItems: int, pageNumber: int, - includeHeader: bool) -> str: + includeHeader: bool, systemLanguage: str) -> str: """Returns an RSS version 2 feed containing posts """ if ' ' in nickname or '@' in nickname or \ @@ -585,7 +608,7 @@ def htmlBlogPageRSS2(authorized: bool, session, httpPrefix, translate, nickname, domain, domainFull, item, - None, True) + None, True, systemLanguage) if includeHeader: return blogRSS2 + rss2Footer() @@ -596,7 +619,8 @@ def htmlBlogPageRSS2(authorized: bool, session, def htmlBlogPageRSS3(authorized: bool, session, baseDir: str, httpPrefix: str, translate: {}, nickname: str, domain: str, port: int, - noOfItems: int, pageNumber: int) -> str: + noOfItems: int, pageNumber: int, + systemLanguage: str) -> str: """Returns an RSS version 3 feed containing posts """ if ' ' in nickname or '@' in nickname or \ @@ -630,7 +654,8 @@ def htmlBlogPageRSS3(authorized: bool, session, httpPrefix, translate, nickname, domain, domainFull, item, - None, True) + None, True, + systemLanguage) return blogRSS3 @@ -670,7 +695,8 @@ def htmlBlogView(authorized: bool, session, baseDir: str, httpPrefix: str, translate: {}, domain: str, port: int, noOfItems: int, - peertubeInstances: []) -> str: + peertubeInstances: [], systemLanguage: str, + personCache: {}) -> str: """Show the blog main page """ blogStr = '' @@ -688,7 +714,8 @@ def htmlBlogView(authorized: bool, return htmlBlogPage(authorized, session, baseDir, httpPrefix, translate, nickname, domain, port, - noOfItems, 1, peertubeInstances) + noOfItems, 1, peertubeInstances, + systemLanguage, personCache) domainFull = getFullDomain(domain, port) @@ -714,7 +741,7 @@ def htmlEditBlog(mediaInstance: bool, translate: {}, path: str, pageNumber: int, nickname: str, domain: str, - postUrl: str) -> str: + postUrl: str, systemLanguage: str) -> str: """Edit a blog post after it was created """ postFilename = locatePost(baseDir, nickname, domain, postUrl) @@ -828,17 +855,15 @@ def htmlEditBlog(mediaInstance: bool, translate: {}, editBlogForm += \ ' ' editBlogForm += '' - editBlogForm += '
' + editBlogForm += '
' messageBoxHeight = 800 - contentStr = postJsonObject['object']['content'] + contentStr = getBaseContentFromPost(postJsonObject, systemLanguage) contentStr = contentStr.replace('

', '').replace('

', '\n') editBlogForm += \ - ' ' + editTextArea(placeholderMessage, 'message', contentStr, + messageBoxHeight, '', True) editBlogForm += dateAndLocation if not mediaInstance: editBlogForm += editBlogImageSection @@ -877,8 +902,8 @@ def pathContainsBlogLink(baseDir: str, return None, None if '#' + userEnding2[1] + '.' not in open(blogIndexFilename).read(): return None, None - messageId = httpPrefix + '://' + domainFull + \ - '/users/' + nickname + '/statuses/' + userEnding2[1] + messageId = localActorUrl(httpPrefix, nickname, domainFull) + \ + '/statuses/' + userEnding2[1] return locatePost(baseDir, nickname, domain, messageId), nickname diff --git a/bookmarks.py b/bookmarks.py index cc27e1aba..921c057e4 100644 --- a/bookmarks.py +++ b/bookmarks.py @@ -25,6 +25,7 @@ from utils import loadJson from utils import saveJson from utils import hasObjectDict from utils import acctDir +from utils import localActorUrl from posts import getPersonBox from session import postJson @@ -242,7 +243,7 @@ def bookmark(recentPostsCache: {}, newBookmarkJson = { "@context": "https://www.w3.org/ns/activitystreams", 'type': 'Bookmark', - 'actor': httpPrefix + '://' + fullDomain + '/users/' + nickname, + 'actor': localActorUrl(httpPrefix, nickname, fullDomain), 'object': objectUrl } if ccList: @@ -301,10 +302,10 @@ def undoBookmark(recentPostsCache: {}, newUndoBookmarkJson = { "@context": "https://www.w3.org/ns/activitystreams", 'type': 'Undo', - 'actor': httpPrefix + '://' + fullDomain + '/users/' + nickname, + 'actor': localActorUrl(httpPrefix, nickname, fullDomain), 'object': { 'type': 'Bookmark', - 'actor': httpPrefix + '://' + fullDomain + '/users/' + nickname, + 'actor': localActorUrl(httpPrefix, nickname, fullDomain), 'object': objectUrl } } @@ -356,7 +357,7 @@ def sendBookmarkViaServer(baseDir: str, session, domainFull = getFullDomain(domain, fromPort) - actor = httpPrefix + '://' + domainFull + '/users/' + nickname + actor = localActorUrl(httpPrefix, nickname, domainFull) newBookmarkJson = { "@context": "https://www.w3.org/ns/activitystreams", @@ -376,7 +377,7 @@ def sendBookmarkViaServer(baseDir: str, session, # lookup the inbox for the To handle wfRequest = webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - domain, projectVersion, debug) + domain, projectVersion, debug, False) if not wfRequest: if debug: print('DEBUG: bookmark webfinger failed for ' + handle) @@ -441,7 +442,7 @@ def sendUndoBookmarkViaServer(baseDir: str, session, domainFull = getFullDomain(domain, fromPort) - actor = httpPrefix + '://' + domainFull + '/users/' + nickname + actor = localActorUrl(httpPrefix, nickname, domainFull) newBookmarkJson = { "@context": "https://www.w3.org/ns/activitystreams", @@ -461,7 +462,7 @@ def sendUndoBookmarkViaServer(baseDir: str, session, # lookup the inbox for the To handle wfRequest = webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - domain, projectVersion, debug) + domain, projectVersion, debug, False) if not wfRequest: if debug: print('DEBUG: unbookmark webfinger failed for ' + handle) diff --git a/cache.py b/cache.py index 8d6291316..9ba0111fb 100644 --- a/cache.py +++ b/cache.py @@ -10,9 +10,11 @@ __module_group__ = "Core" import os import datetime from session import urlExists +from session import getJson from utils import loadJson from utils import saveJson from utils import getFileCaseInsensitive +from utils import getUserPaths def _removePersonFromCache(baseDir: str, personUrl: str, @@ -132,3 +134,52 @@ def getWebfingerFromCache(handle: str, cachedWebfingers: {}) -> {}: if cachedWebfingers.get(handle): return cachedWebfingers[handle] return None + + +def getPersonPubKey(baseDir: str, session, personUrl: str, + personCache: {}, debug: bool, + projectVersion: str, httpPrefix: str, + domain: str, onionDomain: str) -> str: + if not personUrl: + return None + personUrl = personUrl.replace('#main-key', '') + usersPaths = getUserPaths() + for possibleUsersPath in usersPaths: + if personUrl.endswith(possibleUsersPath + 'inbox'): + if debug: + print('DEBUG: Obtaining public key for shared inbox') + personUrl = \ + personUrl.replace(possibleUsersPath + 'inbox', '/inbox') + break + personJson = \ + getPersonFromCache(baseDir, personUrl, personCache, True) + if not personJson: + if debug: + print('DEBUG: Obtaining public key for ' + personUrl) + personDomain = domain + if onionDomain: + if '.onion/' in personUrl: + personDomain = onionDomain + profileStr = 'https://www.w3.org/ns/activitystreams' + asHeader = { + 'Accept': 'application/activity+json; profile="' + profileStr + '"' + } + personJson = \ + getJson(session, personUrl, asHeader, None, debug, + projectVersion, httpPrefix, personDomain) + if not personJson: + return None + pubKey = None + if personJson.get('publicKey'): + if personJson['publicKey'].get('publicKeyPem'): + pubKey = personJson['publicKey']['publicKeyPem'] + else: + if personJson.get('publicKeyPem'): + pubKey = personJson['publicKeyPem'] + + if not pubKey: + if debug: + print('DEBUG: Public key not found for ' + personUrl) + + storePersonInCache(baseDir, personUrl, personJson, personCache, True) + return pubKey diff --git a/categories.py b/categories.py index 06f0d4056..f60520b7e 100644 --- a/categories.py +++ b/categories.py @@ -86,7 +86,7 @@ def getHashtagCategories(baseDir: str, return hashtagCategories -def _updateHashtagCategories(baseDir: str) -> None: +def updateHashtagCategories(baseDir: str) -> None: """Regenerates the list of hashtag categories """ categoryListFilename = baseDir + '/accounts/categoryList.txt' @@ -129,7 +129,7 @@ def _validHashtagCategory(category: str) -> bool: def setHashtagCategory(baseDir: str, hashtag: str, category: str, - force: bool = False) -> bool: + update: bool, force: bool = False) -> bool: """Sets the category for the hashtag """ if not _validHashtagCategory(category): @@ -155,7 +155,8 @@ def setHashtagCategory(baseDir: str, hashtag: str, category: str, return False with open(categoryFilename, 'w+') as fp: fp.write(category) - _updateHashtagCategories(baseDir) + if update: + updateHashtagCategories(baseDir) return True return False diff --git a/city.py b/city.py index 161ef9b5a..a780996f6 100644 --- a/city.py +++ b/city.py @@ -292,6 +292,7 @@ def getSpoofedCity(city: str, baseDir: str, nickname: str, domain: str) -> str: """Returns the name of the city to use as a GPS spoofing location for image metadata """ + city = '' cityFilename = acctDir(baseDir, nickname, domain) + '/city.txt' if os.path.isfile(cityFilename): with open(cityFilename, 'r') as fp: diff --git a/content.py b/content.py index 3f3b1d8ec..0c13f0e23 100644 --- a/content.py +++ b/content.py @@ -21,6 +21,8 @@ from utils import dangerousMarkup from utils import isPGPEncrypted from utils import containsPGPPublicKey from utils import acctDir +from utils import isfloat +from utils import getCurrencies from petnames import getPetName @@ -497,7 +499,7 @@ def _addMention(wordStr: str, httpPrefix: str, following: str, petnames: str, followStr = follow.replace('\n', '').replace('\r', '') replaceDomain = followStr.split('@')[1] recipientActor = httpPrefix + "://" + \ - replaceDomain + "/users/" + possibleNickname + replaceDomain + "/@" + possibleNickname if recipientActor not in recipients: recipients.append(recipientActor) tags[wordStr] = { @@ -524,7 +526,7 @@ def _addMention(wordStr: str, httpPrefix: str, following: str, petnames: str, replaceNickname = followStr.split('@')[0] replaceDomain = followStr.split('@')[1] recipientActor = httpPrefix + "://" + \ - replaceDomain + "/users/" + replaceNickname + replaceDomain + "/@" + replaceNickname if recipientActor not in recipients: recipients.append(recipientActor) tags[wordStr] = { @@ -556,7 +558,7 @@ def _addMention(wordStr: str, httpPrefix: str, following: str, petnames: str, if follow.replace('\n', '').replace('\r', '') != possibleHandle: continue recipientActor = httpPrefix + "://" + \ - possibleDomain + "/users/" + possibleNickname + possibleDomain + "/@" + possibleNickname if recipientActor not in recipients: recipients.append(recipientActor) tags[wordStr] = { @@ -574,7 +576,7 @@ def _addMention(wordStr: str, httpPrefix: str, following: str, petnames: str, if not (possibleDomain == 'localhost' or '.' in possibleDomain): return False recipientActor = httpPrefix + "://" + \ - possibleDomain + "/users/" + possibleNickname + possibleDomain + "/@" + possibleNickname if recipientActor not in recipients: recipients.append(recipientActor) tags[wordStr] = { @@ -930,6 +932,16 @@ def saveMediaInFormPOST(mediaBytes, debug: bool, Returns the filename and attachment type """ if not mediaBytes: + if filenameBase: + # remove any existing files + extensionTypes = getImageExtensions() + for ex in extensionTypes: + possibleOtherFormat = filenameBase + '.' + ex + if os.path.isfile(possibleOtherFormat): + os.remove(possibleOtherFormat) + if os.path.isfile(filenameBase): + os.remove(filenameBase) + if debug: print('DEBUG: No media found within POST') return None, None @@ -951,6 +963,7 @@ def saveMediaInFormPOST(mediaBytes, debug: bool, 'ogv': 'video/ogv', 'mp3': 'audio/mpeg', 'ogg': 'audio/ogg', + 'flac': 'audio/flac', 'zip': 'application/zip' } detectedExtension = None @@ -1085,3 +1098,21 @@ def limitRepeatedWords(text: str, maxRepeats: int) -> str: for word, item in replacements.items(): text = text.replace(item[0], item[1]) return text + + +def getPriceFromString(priceStr: str) -> (str, str): + """Returns the item price and currency + """ + currencies = getCurrencies() + for symbol, name in currencies.items(): + if symbol in priceStr: + price = priceStr.replace(symbol, '') + if isfloat(price): + return price, name + elif name in priceStr: + price = priceStr.replace(name, '') + if isfloat(price): + return price, name + if isfloat(priceStr): + return priceStr, "EUR" + return "0.00", "EUR" diff --git a/context.py b/context.py index 9afa78d79..20efb02d9 100644 --- a/context.py +++ b/context.py @@ -14,6 +14,7 @@ validContexts = ( "https://w3id.org/security/v1", "*/apschema/v1.9", "*/apschema/v1.21", + "*/apschema/v1.20", "*/litepub-0.1.jsonld", "https://litepub.social/litepub/context.jsonld" ) @@ -100,6 +101,43 @@ def getApschemaV1_9() -> {}: } +def getApschemaV1_20() -> {}: + # https://domain/apschema/v1.20 + return { + "@context": + { + "as": "https://www.w3.org/ns/activitystreams#", + "zot": "https://zap.dog/apschema#", + "toot": "http://joinmastodon.org/ns#", + "ostatus": "http://ostatus.org#", + "schema": "http://schema.org#", + "litepub": "http://litepub.social/ns#", + "sm": "http://smithereen.software/ns#", + "conversation": "ostatus:conversation", + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "oauthRegistrationEndpoint": "litepub:oauthRegistrationEndpoint", + "sensitive": "as:sensitive", + "movedTo": "as:movedTo", + "copiedTo": "as:copiedTo", + "alsoKnownAs": "as:alsoKnownAs", + "inheritPrivacy": "as:inheritPrivacy", + "EmojiReact": "as:EmojiReact", + "commentPolicy": "zot:commentPolicy", + "topicalCollection": "zot:topicalCollection", + "eventRepeat": "zot:eventRepeat", + "emojiReaction": "zot:emojiReaction", + "expires": "zot:expires", + "directMessage": "zot:directMessage", + "Category": "zot:Category", + "replyTo": "zot:replyTo", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value", + "discoverable": "toot:discoverable", + "wall": "sm:wall" + } + } + + def getApschemaV1_21() -> {}: # https://domain/apschema/v1.21 return { diff --git a/conversation.py b/conversation.py new file mode 100644 index 000000000..32ecd0e08 --- /dev/null +++ b/conversation.py @@ -0,0 +1,78 @@ +__filename__ = "conversation.py" +__author__ = "Bob Mottram" +__license__ = "AGPL3+" +__version__ = "1.2.0" +__maintainer__ = "Bob Mottram" +__email__ = "bob@freedombone.net" +__status__ = "Production" +__module_group__ = "Timeline" + +import os +from utils import hasObjectDict +from utils import acctDir + + +def updateConversation(baseDir: str, nickname: str, domain: str, + postJsonObject: {}) -> bool: + """Ads a post to a conversation index in the /conversation subdirectory + """ + if not hasObjectDict(postJsonObject): + return False + if not postJsonObject['object'].get('conversation'): + return False + if not postJsonObject['object'].get('id'): + return False + conversationDir = acctDir(baseDir, nickname, domain) + '/conversation' + if not os.path.isdir(conversationDir): + os.mkdir(conversationDir) + conversationId = postJsonObject['object']['conversation'] + conversationId = conversationId.replace('/', '#') + postId = postJsonObject['object']['id'] + conversationFilename = conversationDir + '/' + conversationId + if not os.path.isfile(conversationFilename): + try: + with open(conversationFilename, 'w+') as fp: + fp.write(postId + '\n') + return True + except BaseException: + pass + elif postId + '\n' not in open(conversationFilename).read(): + try: + with open(conversationFilename, 'a+') as fp: + fp.write(postId + '\n') + return True + except BaseException: + pass + return False + + +def muteConversation(baseDir: str, nickname: str, domain: str, + conversationId: str) -> None: + """Mutes the given conversation + """ + conversationDir = acctDir(baseDir, nickname, domain) + '/conversation' + conversationFilename = \ + conversationDir + '/' + conversationId.replace('/', '#') + if not os.path.isfile(conversationFilename): + return + if os.path.isfile(conversationFilename + '.muted'): + return + with open(conversationFilename + '.muted', 'w+') as fp: + fp.write('\n') + + +def unmuteConversation(baseDir: str, nickname: str, domain: str, + conversationId: str) -> None: + """Unmutes the given conversation + """ + conversationDir = acctDir(baseDir, nickname, domain) + '/conversation' + conversationFilename = \ + conversationDir + '/' + conversationId.replace('/', '#') + if not os.path.isfile(conversationFilename): + return + if not os.path.isfile(conversationFilename + '.muted'): + return + try: + os.remove(conversationFilename + '.muted') + except BaseException: + pass diff --git a/daemon.py b/daemon.py index 4f247e267..3cd3036d3 100644 --- a/daemon.py +++ b/daemon.py @@ -19,7 +19,7 @@ from functools import partial import pyqrcode # for saving images from hashlib import sha256 -from hashlib import sha1 +from hashlib import md5 from session import createSession from webfinger import webfingerMeta from webfinger import webfingerNodeInfo @@ -50,6 +50,8 @@ from matrix import getMatrixAddress from matrix import setMatrixAddress from donate import getDonationUrl from donate import setDonationUrl +from donate import getWebsite +from donate import setWebsite from person import setPersonNotes from person import getDefaultPersonContext from person import savePersonQrcode @@ -92,7 +94,6 @@ from inbox import runInboxQueue from inbox import runInboxQueueWatchdog from inbox import savePostToInboxQueue from inbox import populateReplies -from inbox import getPersonPubKey from follow import isFollowingActor from follow import getFollowingFeed from follow import sendFollowRequest @@ -111,6 +112,8 @@ from auth import authorizeBasic from auth import storeBasicCredentials from threads import threadWithTrace from threads import removeDormantThreads +from media import processMetaData +from media import convertImageToLowBandwidth from media import replaceYouTube from media import attachMedia from media import pathIsVideo @@ -147,6 +150,7 @@ from webapp_utils import getAvatarImageUrl from webapp_utils import htmlHashtagBlocked from webapp_utils import htmlFollowingList from webapp_utils import setBlogAddress +from webapp_utils import htmlShowShare from webapp_calendar import htmlCalendarDeleteConfirm from webapp_calendar import htmlCalendar from webapp_about import htmlAbout @@ -157,6 +161,7 @@ from webapp_confirm import htmlConfirmRemoveSharedItem from webapp_confirm import htmlConfirmUnblock from webapp_person_options import htmlPersonOptions from webapp_timeline import htmlShares +from webapp_timeline import htmlWanted from webapp_timeline import htmlInbox from webapp_timeline import htmlBookmarks from webapp_timeline import htmlInboxDMs @@ -203,11 +208,29 @@ from webapp_welcome import htmlWelcomeScreen from webapp_welcome import isWelcomeScreenComplete from webapp_welcome_profile import htmlWelcomeProfile from webapp_welcome_final import htmlWelcomeFinal +from shares import mergeSharedItemTokens +from shares import runFederatedSharesDaemon +from shares import runFederatedSharesWatchdog +from shares import updateSharedItemFederationToken +from shares import createSharedItemFederationToken +from shares import authorizeSharedItems +from shares import generateSharedItemFederationTokens from shares import getSharesFeedForPerson from shares import addShare -from shares import removeShare +from shares import removeSharedItem from shares import expireShares +from shares import sharesCatalogEndpoint +from shares import sharesCatalogAccountEndpoint +from shares import sharesCatalogCSVEndpoint from categories import setHashtagCategory +from categories import updateHashtagCategories +from languages import getActorLanguages +from languages import setActorLanguages +from utils import localActorUrl +from utils import isfloat +from utils import validPassword +from utils import removeLineEndings +from utils import getBaseContentFromPost from utils import acctDir from utils import getImageExtensionFromMimeType from utils import getImageMimeType @@ -257,18 +280,20 @@ from utils import isSuspended from utils import dangerousMarkup from utils import refreshNewswire from utils import isImageFile +from utils import hasGroupType from manualapprove import manualDenyFollowRequest from manualapprove import manualApproveFollowRequest from announce import createAnnounce +from content import getPriceFromString from content import replaceEmojiFromTags from content import addHtmlTags from content import extractMediaInFormPOST from content import saveMediaInFormPOST from content import extractTextFieldsInPOST -from media import processMetaData from cache import checkForChangedActor from cache import storePersonInCache from cache import getPersonFromCache +from cache import getPersonPubKey from httpsig import verifyPostHeaders from theme import importTheme from theme import exportTheme @@ -402,6 +427,7 @@ class PubServer(BaseHTTPRequestHandler): eventDate = None eventTime = None location = None + conversationId = None city = getSpoofedCity(self.server.city, self.server.baseDir, nickname, self.server.domain) @@ -421,7 +447,10 @@ class PubServer(BaseHTTPRequestHandler): schedulePost, eventDate, eventTime, - location, False) + location, False, + self.server.systemLanguage, + conversationId, + self.server.lowBandwidth) if messageJson: # name field contains the answer messageJson['object']['name'] = answer @@ -509,6 +538,16 @@ class PubServer(BaseHTTPRequestHandler): print('Blocked User agent: ' + agentDomain) return blockedUA + def _requestCSV(self) -> bool: + """Should a csv response be given? + """ + if not self.headers.get('Accept'): + return False + acceptStr = self.headers['Accept'] + if 'text/csv' in acceptStr: + return True + return False + def _requestHTTP(self) -> bool: """Should a http response be given? """ @@ -648,11 +687,15 @@ class PubServer(BaseHTTPRequestHandler): self.end_headers() def _set_headers_base(self, fileFormat: str, length: int, cookie: str, - callingDomain: str) -> None: + callingDomain: str, permissive: bool) -> None: self.send_response(200) self.send_header('Content-type', fileFormat) if length > -1: self.send_header('Content-Length', str(length)) + self.send_header('Host', callingDomain) + if permissive: + self.send_header('Access-Control-Allow-Origin', '*') + return if cookie: cookieStr = cookie if 'HttpOnly;' not in cookieStr: @@ -660,28 +703,32 @@ class PubServer(BaseHTTPRequestHandler): cookieStr += '; Secure' cookieStr += '; HttpOnly; SameSite=Strict' self.send_header('Cookie', cookieStr) - self.send_header('Host', callingDomain) + self.send_header('Origin', self.server.domainFull) self.send_header('InstanceID', self.server.instanceId) self.send_header('X-Clacks-Overhead', 'GNU Natalie Nguyen') self.send_header('Cache-Control', 'max-age=0') self.send_header('Cache-Control', 'public') def _set_headers(self, fileFormat: str, length: int, cookie: str, - callingDomain: str) -> None: - self._set_headers_base(fileFormat, length, cookie, callingDomain) + callingDomain: str, permissive: bool) -> None: + self._set_headers_base(fileFormat, length, cookie, callingDomain, + permissive) self.end_headers() def _set_headers_head(self, fileFormat: str, length: int, etag: str, - callingDomain: str) -> None: - self._set_headers_base(fileFormat, length, None, callingDomain) + callingDomain: str, permissive: bool) -> None: + self._set_headers_base(fileFormat, length, None, callingDomain, + permissive) if etag: - self.send_header('ETag', etag) + self.send_header('ETag', '"' + etag + '"') self.end_headers() def _set_headers_etag(self, mediaFilename: str, fileFormat: str, - data, cookie: str, callingDomain: str) -> None: + data, cookie: str, callingDomain: str, + permissive: bool, lastModified: str) -> None: datalen = len(data) - self._set_headers_base(fileFormat, datalen, cookie, callingDomain) + self._set_headers_base(fileFormat, datalen, cookie, callingDomain, + permissive) # self.send_header('Cache-Control', 'public, max-age=86400') etag = None if os.path.isfile(mediaFilename + '.etag'): @@ -691,14 +738,16 @@ class PubServer(BaseHTTPRequestHandler): except BaseException: pass if not etag: - etag = sha1(data).hexdigest() # nosec + etag = md5(data).hexdigest() # nosec try: with open(mediaFilename + '.etag', 'w+') as etagFile: etagFile.write(etag) except BaseException: pass if etag: - self.send_header('ETag', etag) + self.send_header('ETag', '"' + etag + '"') + if lastModified: + self.send_header('last-modified', lastModified) self.end_headers() def _etag_exists(self, mediaFilename: str) -> bool: @@ -711,7 +760,7 @@ class PubServer(BaseHTTPRequestHandler): etagHeader = 'If-none-match' if self.headers.get(etagHeader): - oldEtag = self.headers['If-None-Match'] + oldEtag = self.headers[etagHeader].replace('"', '') if os.path.isfile(mediaFilename + '.etag'): # load the etag from file currEtag = '' @@ -720,7 +769,7 @@ class PubServer(BaseHTTPRequestHandler): currEtag = etagFile.read() except BaseException: pass - if oldEtag == currEtag: + if currEtag and oldEtag == currEtag: # The file has not changed return True return False @@ -848,7 +897,7 @@ class PubServer(BaseHTTPRequestHandler): msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/plain; charset=utf-8', msglen, - None, self.server.domainFull) + None, self.server.domainFull, True) self._write(msg) return True @@ -909,13 +958,13 @@ class PubServer(BaseHTTPRequestHandler): if self._hasAccept(callingDomain): if 'application/ld+json' in self.headers['Accept']: self._set_headers('application/ld+json', msglen, - None, callingDomain) + None, callingDomain, True) else: self._set_headers('application/json', msglen, - None, callingDomain) + None, callingDomain, True) else: self._set_headers('application/ld+json', msglen, - None, callingDomain) + None, callingDomain, True) self._write(msg) if sendJsonStr: print(sendJsonStr) @@ -978,13 +1027,13 @@ class PubServer(BaseHTTPRequestHandler): if self._hasAccept(callingDomain): if 'application/ld+json' in self.headers['Accept']: self._set_headers('application/ld+json', msglen, - None, callingDomain) + None, callingDomain, True) else: self._set_headers('application/json', msglen, - None, callingDomain) + None, callingDomain, True) else: self._set_headers('application/ld+json', msglen, - None, callingDomain) + None, callingDomain, True) self._write(msg) print('nodeinfo sent') return True @@ -1016,12 +1065,20 @@ class PubServer(BaseHTTPRequestHandler): msg = wfResult.encode('utf-8') msglen = len(msg) self._set_headers('application/xrd+xml', msglen, - None, callingDomain) + None, callingDomain, True) self._write(msg) return True self._404() return True - if self.path.startswith('/.well-known/nodeinfo'): + if self.path.startswith('/api/statusnet') or \ + self.path.startswith('/api/gnusocial') or \ + self.path.startswith('/siteinfo') or \ + self.path.startswith('/poco') or \ + self.path.startswith('/friendi'): + self._404() + return True + if self.path.startswith('/.well-known/nodeinfo') or \ + self.path.startswith('/.well-known/x-nodeinfo'): if callingDomain.endswith('.onion') and \ self.server.onionDomain: wfResult = \ @@ -1040,13 +1097,13 @@ class PubServer(BaseHTTPRequestHandler): if self._hasAccept(callingDomain): if 'application/ld+json' in self.headers['Accept']: self._set_headers('application/ld+json', msglen, - None, callingDomain) + None, callingDomain, True) else: self._set_headers('application/json', msglen, - None, callingDomain) + None, callingDomain, True) else: self._set_headers('application/ld+json', msglen, - None, callingDomain) + None, callingDomain, True) self._write(msg) return True self._404() @@ -1063,7 +1120,7 @@ class PubServer(BaseHTTPRequestHandler): msg = json.dumps(wfResult).encode('utf-8') msglen = len(msg) self._set_headers('application/jrd+json', msglen, - None, callingDomain) + None, callingDomain, True) self._write(msg) else: if self.server.debug: @@ -1109,7 +1166,10 @@ class PubServer(BaseHTTPRequestHandler): self.server.YTReplacementDomain, self.server.showPublishedDateOnly, self.server.allowLocalNetworkAccess, - city) + city, self.server.systemLanguage, + self.server.sharedItemsFederatedDomains, + self.server.sharedItemFederationTokens, + self.server.lowBandwidth) def _postToOutboxThread(self, messageJson: {}) -> bool: """Creates a thread to send a post @@ -1267,6 +1327,8 @@ class PubServer(BaseHTTPRequestHandler): headersDict['signature'] = self.headers['signature'] if self.headers.get('Date'): headersDict['Date'] = self.headers['Date'] + elif self.headers.get('date'): + headersDict['Date'] = self.headers['date'] if self.headers.get('digest'): headersDict['digest'] = self.headers['digest'] if self.headers.get('Collection-Synchronization'): @@ -1307,7 +1369,8 @@ class PubServer(BaseHTTPRequestHandler): headersDict, self.path, self.server.debug, - self.server.blockedCache) + self.server.blockedCache, + self.server.systemLanguage) if queueFilename: # add json to the queue if queueFilename not in self.server.inboxQueue: @@ -1484,6 +1547,22 @@ class PubServer(BaseHTTPRequestHandler): return self.server.lastLoginTime = int(time.time()) if register: + if not validPassword(loginPassword): + self.server.POSTbusy = False + if callingDomain.endswith('.onion') and onionDomain: + self._redirect_headers('http://' + onionDomain + + '/login', cookie, + callingDomain) + elif (callingDomain.endswith('.i2p') and i2pDomain): + self._redirect_headers('http://' + i2pDomain + + '/login', cookie, + callingDomain) + else: + self._redirect_headers(httpPrefix + '://' + + domainFull + '/login', + cookie, callingDomain) + return + if not registerAccount(baseDir, httpPrefix, domain, port, loginNickname, loginPassword, self.server.manualFollowerApproval): @@ -1695,7 +1774,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.domain, self.server.port, searchHandle, - self.server.debug) + self.server.debug, + self.server.systemLanguage) else: msg = \ htmlModerationInfo(self.server.cssCache, @@ -2226,12 +2306,15 @@ class PubServer(BaseHTTPRequestHandler): # person options screen, block button # See htmlPersonOptions if '&submitBlock=' in optionsConfirmParams: - if debug: - print('Adding block by ' + chooserNickname + - ' of ' + optionsActor) - addBlock(baseDir, chooserNickname, - domain, - optionsNickname, optionsDomainFull) + print('Adding block by ' + chooserNickname + + ' of ' + optionsActor) + if addBlock(baseDir, chooserNickname, + domain, + optionsNickname, optionsDomainFull): + # send block activity + self._sendBlock(httpPrefix, + chooserNickname, domainFull, + optionsNickname, optionsDomainFull) # person options screen, unblock button # See htmlPersonOptions @@ -2247,7 +2330,7 @@ class PubServer(BaseHTTPRequestHandler): optionsAvatarUrl).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self.server.POSTbusy = False return @@ -2266,7 +2349,7 @@ class PubServer(BaseHTTPRequestHandler): optionsAvatarUrl).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self.server.POSTbusy = False return @@ -2284,7 +2367,7 @@ class PubServer(BaseHTTPRequestHandler): optionsAvatarUrl).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self.server.POSTbusy = False return @@ -2305,7 +2388,7 @@ class PubServer(BaseHTTPRequestHandler): accessKeys = self.server.keyShortcuts[nickname] customSubmitText = getConfigParam(baseDir, 'customSubmitText') - + conversationId = None msg = htmlNewPost(self.server.cssCache, False, self.server.translate, baseDir, @@ -2320,10 +2403,11 @@ class PubServer(BaseHTTPRequestHandler): self.server.newswire, self.server.themeName, True, accessKeys, - customSubmitText).encode('utf-8') + customSubmitText, + conversationId).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self.server.POSTbusy = False return @@ -2343,10 +2427,11 @@ class PubServer(BaseHTTPRequestHandler): domain, self.server.port, optionsActor, - self.server.debug).encode('utf-8') + self.server.debug, + self.server.systemLanguage).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self.server.POSTbusy = False return @@ -2417,7 +2502,7 @@ class PubServer(BaseHTTPRequestHandler): accessKeys = self.server.keyShortcuts[nickname] customSubmitText = getConfigParam(baseDir, 'customSubmitText') - + conversationId = None msg = htmlNewPost(self.server.cssCache, False, self.server.translate, baseDir, @@ -2431,10 +2516,11 @@ class PubServer(BaseHTTPRequestHandler): self.server.newswire, self.server.themeName, True, accessKeys, - customSubmitText).encode('utf-8') + customSubmitText, + conversationId).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self.server.POSTbusy = False return @@ -2502,8 +2588,7 @@ class PubServer(BaseHTTPRequestHandler): print(followerNickname + ' stops following ' + followingActor) followActor = \ - httpPrefix + '://' + domainFull + \ - '/users/' + followerNickname + localActorUrl(httpPrefix, followerNickname, domainFull) statusNumber, published = getStatusNumber() followId = followActor + '/statuses/' + str(statusNumber) unfollowJson = { @@ -2520,9 +2605,13 @@ class PubServer(BaseHTTPRequestHandler): } pathUsersSection = path.split('/users/')[1] self.postToNickname = pathUsersSection.split('/')[0] + groupAccount = hasGroupType(self.server.baseDir, + followingActor, + self.server.personCache) unfollowAccount(self.server.baseDir, self.postToNickname, self.server.domain, - followingNickname, followingDomainFull) + followingNickname, followingDomainFull, + self.server.debug, groupAccount) self._postToOutboxThread(unfollowJson) if callingDomain.endswith('.onion') and onionDomain: @@ -2690,13 +2779,16 @@ class PubServer(BaseHTTPRequestHandler): if debug: print('You cannot block yourself!') else: - if debug: - print('Adding block by ' + blockerNickname + - ' of ' + blockingActor) - addBlock(baseDir, blockerNickname, - domain, - blockingNickname, - blockingDomainFull) + print('Adding block by ' + blockerNickname + + ' of ' + blockingActor) + if addBlock(baseDir, blockerNickname, + domain, + blockingNickname, + blockingDomainFull): + # send block activity + self._sendBlock(httpPrefix, + blockerNickname, domainFull, + blockingNickname, blockingDomainFull) if callingDomain.endswith('.onion') and onionDomain: originPathStr = 'http://' + onionDomain + usersPath elif (callingDomain.endswith('.i2p') and i2pDomain): @@ -2866,7 +2958,9 @@ class PubServer(BaseHTTPRequestHandler): self.server.showPublishedDateOnly, self.server.peertubeInstances, self.server.allowLocalNetworkAccess, - self.server.themeName) + self.server.themeName, + self.server.systemLanguage, + self.server.maxLikeCount) if hashtagStr: msg = hashtagStr.encode('utf-8') msglen = len(msg) @@ -2895,10 +2989,10 @@ class PubServer(BaseHTTPRequestHandler): self._write(msg) self.server.POSTbusy = False return - elif searchStr.startswith('!'): + elif searchStr.startswith("'"): # your post history search nickname = getNicknameFromActor(actorStr) - searchStr = searchStr.replace('!', '', 1).strip() + searchStr = searchStr.replace("'", '', 1).strip() historyStr = \ htmlHistorySearch(self.server.cssCache, self.server.translate, @@ -2920,7 +3014,9 @@ class PubServer(BaseHTTPRequestHandler): self.server.showPublishedDateOnly, self.server.peertubeInstances, self.server.allowLocalNetworkAccess, - self.server.themeName, 'outbox') + self.server.themeName, 'outbox', + self.server.systemLanguage, + self.server.maxLikeCount) if historyStr: msg = historyStr.encode('utf-8') msglen = len(msg) @@ -2954,7 +3050,9 @@ class PubServer(BaseHTTPRequestHandler): self.server.showPublishedDateOnly, self.server.peertubeInstances, self.server.allowLocalNetworkAccess, - self.server.themeName, 'bookmarks') + self.server.themeName, 'bookmarks', + self.server.systemLanguage, + self.server.maxLikeCount) if bookmarksStr: msg = bookmarksStr.encode('utf-8') msglen = len(msg) @@ -2994,10 +3092,11 @@ class PubServer(BaseHTTPRequestHandler): searchNickname = getNicknameFromActor(searchStr) searchDomain, searchPort = \ getDomainFromActor(searchStr) + searchDomainFull = \ + getFullDomain(searchDomain, searchPort) actor = \ - httpPrefix + '://' + \ - getFullDomain(searchDomain, searchPort) + \ - '/users/' + searchNickname + localActorUrl(httpPrefix, searchNickname, + searchDomainFull) else: actor = searchStr avatarUrl = \ @@ -3048,7 +3147,9 @@ class PubServer(BaseHTTPRequestHandler): self.server.peertubeInstances, allowLocalNetworkAccess, self.server.themeName, - accessKeys) + accessKeys, + self.server.systemLanguage, + self.server.maxLikeCount) if profileStr: msg = profileStr.encode('utf-8') msglen = len(msg) @@ -3084,8 +3185,33 @@ class PubServer(BaseHTTPRequestHandler): self._write(msg) self.server.POSTbusy = False return + elif searchStr.startswith('.'): + # wanted items search + sharedItemsFederatedDomains = \ + self.server.sharedItemsFederatedDomains + wantedItemsStr = \ + htmlSearchSharedItems(self.server.cssCache, + self.server.translate, + baseDir, + searchStr[1:], pageNumber, + maxPostsInFeed, + httpPrefix, + domainFull, + actorStr, callingDomain, + sharedItemsFederatedDomains, + 'wanted') + if wantedItemsStr: + msg = wantedItemsStr.encode('utf-8') + msglen = len(msg) + self._login_headers('text/html', + msglen, callingDomain) + self._write(msg) + self.server.POSTbusy = False + return else: # shared items search + sharedItemsFederatedDomains = \ + self.server.sharedItemsFederatedDomains sharedItemsStr = \ htmlSearchSharedItems(self.server.cssCache, self.server.translate, @@ -3094,7 +3220,9 @@ class PubServer(BaseHTTPRequestHandler): maxPostsInFeed, httpPrefix, domainFull, - actorStr, callingDomain) + actorStr, callingDomain, + sharedItemsFederatedDomains, + 'shares') if sharedItemsStr: msg = sharedItemsStr.encode('utf-8') msglen = len(msg) @@ -3289,7 +3417,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.POSTbusy = False return - if '&submitYes=' in removeShareConfirmParams: + if '&submitYes=' in removeShareConfirmParams and authorized: removeShareConfirmParams = \ removeShareConfirmParams.replace('+', ' ').strip() removeShareConfirmParams = \ @@ -3297,14 +3425,22 @@ class PubServer(BaseHTTPRequestHandler): shareActor = removeShareConfirmParams.split('actor=')[1] if '&' in shareActor: shareActor = shareActor.split('&')[0] - shareName = removeShareConfirmParams.split('shareName=')[1] - if '&' in shareName: - shareName = shareName.split('&')[0] - shareNickname = getNicknameFromActor(shareActor) - if shareNickname: - shareDomain, sharePort = getDomainFromActor(shareActor) - removeShare(baseDir, - shareNickname, shareDomain, shareName) + adminNickname = getConfigParam(baseDir, 'admin') + adminActor = \ + httpPrefix + '://' + domainFull + '/users' + adminNickname + actor = originPathStr + actorNickname = getNicknameFromActor(actor) + if actor == shareActor or actor == adminActor or \ + isModerator(baseDir, actorNickname): + itemID = removeShareConfirmParams.split('itemID=')[1] + if '&' in itemID: + itemID = itemID.split('&')[0] + shareNickname = getNicknameFromActor(shareActor) + if shareNickname: + shareDomain, sharePort = getDomainFromActor(shareActor) + removeSharedItem(baseDir, + shareNickname, shareDomain, itemID, + httpPrefix, domainFull, 'shares') if callingDomain.endswith('.onion') and onionDomain: originPathStr = 'http://' + onionDomain + usersPath @@ -3314,6 +3450,73 @@ class PubServer(BaseHTTPRequestHandler): cookie, callingDomain) self.server.POSTbusy = False + def _removeWanted(self, callingDomain: str, cookie: str, + authorized: bool, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, + onionDomain: str, i2pDomain: str, + debug: bool) -> None: + """Removes a wanted item + """ + usersPath = path.split('/rmwanted')[0] + originPathStr = httpPrefix + '://' + domainFull + usersPath + + length = int(self.headers['Content-length']) + + try: + removeShareConfirmParams = \ + self.rfile.read(length).decode('utf-8') + except SocketError as e: + if e.errno == errno.ECONNRESET: + print('WARN: POST removeShareConfirmParams ' + + 'connection was reset') + else: + print('WARN: POST removeShareConfirmParams socket error') + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + except ValueError as e: + print('ERROR: POST removeShareConfirmParams rfile.read failed, ' + + str(e)) + self.send_response(400) + self.end_headers() + self.server.POSTbusy = False + return + + if '&submitYes=' in removeShareConfirmParams and authorized: + removeShareConfirmParams = \ + removeShareConfirmParams.replace('+', ' ').strip() + removeShareConfirmParams = \ + urllib.parse.unquote_plus(removeShareConfirmParams) + shareActor = removeShareConfirmParams.split('actor=')[1] + if '&' in shareActor: + shareActor = shareActor.split('&')[0] + adminNickname = getConfigParam(baseDir, 'admin') + adminActor = \ + httpPrefix + '://' + domainFull + '/users' + adminNickname + actor = originPathStr + actorNickname = getNicknameFromActor(actor) + if actor == shareActor or actor == adminActor or \ + isModerator(baseDir, actorNickname): + itemID = removeShareConfirmParams.split('itemID=')[1] + if '&' in itemID: + itemID = itemID.split('&')[0] + shareNickname = getNicknameFromActor(shareActor) + if shareNickname: + shareDomain, sharePort = getDomainFromActor(shareActor) + removeSharedItem(baseDir, + shareNickname, shareDomain, itemID, + httpPrefix, domainFull, 'wanted') + + if callingDomain.endswith('.onion') and onionDomain: + originPathStr = 'http://' + onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and i2pDomain): + originPathStr = 'http://' + i2pDomain + usersPath + self._redirect_headers(originPathStr + '/tlwanted', + cookie, callingDomain) + self.server.POSTbusy = False + def _removePost(self, callingDomain: str, cookie: str, authorized: bool, path: str, baseDir: str, httpPrefix: str, @@ -3606,7 +3809,7 @@ class PubServer(BaseHTTPRequestHandler): categoryStr = fields['hashtagCategory'].lower() if not isBlockedHashtag(baseDir, categoryStr) and \ not isFiltered(baseDir, nickname, domain, categoryStr): - setHashtagCategory(baseDir, hashtag, categoryStr) + setHashtagCategory(baseDir, hashtag, categoryStr, False) else: categoryFilename = baseDir + '/tags/' + hashtag + '.category' if os.path.isfile(categoryFilename): @@ -3905,6 +4108,8 @@ class PubServer(BaseHTTPRequestHandler): newsPostTitle postJsonObject['object']['content'] = \ newsPostContent + contentMap = postJsonObject['object']['contentMap'] + contentMap[self.server.systemLanguage] = newsPostContent # update newswire pubDate = postJsonObject['object']['published'] publishedDate = \ @@ -4085,6 +4290,8 @@ class PubServer(BaseHTTPRequestHandler): city = getSpoofedCity(self.server.city, baseDir, nickname, domain) + if self.server.lowBandwidth: + convertImageToLowBandwidth(filename) processMetaData(baseDir, nickname, domain, filename, postImageFilename, city) if os.path.isfile(postImageFilename): @@ -4214,24 +4421,26 @@ class PubServer(BaseHTTPRequestHandler): setActorSkillLevel(actorJson, skillName, int(skillValue)) skillsStr = self.server.translate['Skills'] + skillsStr = skillsStr.lower() setHashtagCategory(baseDir, skillName, - skillsStr.lower()) + skillsStr, False) skillCtr += 1 if noOfActorSkills(actorJson) != \ actorSkillsCtr: actorChanged = True # change password - if fields.get('password'): - if fields.get('passwordconfirm'): - if actorJson['password'] == \ - fields['passwordconfirm']: - if len(actorJson['password']) > 2: - # set password - pwd = actorJson['password'] - storeBasicCredentials(baseDir, - nickname, - pwd) + if fields.get('password') and \ + fields.get('passwordconfirm'): + fields['password'] = \ + removeLineEndings(fields['password']) + fields['passwordconfirm'] = \ + removeLineEndings(fields['passwordconfirm']) + if validPassword(fields['password']) and \ + fields['password'] == fields['passwordconfirm']: + # set password + storeBasicCredentials(baseDir, nickname, + fields['password']) # change city if fields.get('cityDropdown'): @@ -4417,7 +4626,41 @@ class PubServer(BaseHTTPRequestHandler): setConfigParam(baseDir, 'customSubmitText', '') - # change instance description + # libretranslate URL + currLibretranslateUrl = \ + getConfigParam(baseDir, + 'libretranslateUrl') + if fields.get('libretranslateUrl'): + if fields['libretranslateUrl'] != \ + currLibretranslateUrl: + ltUrl = fields['libretranslateUrl'] + if '://' in ltUrl and \ + '.' in ltUrl: + setConfigParam(baseDir, + 'libretranslateUrl', + ltUrl) + else: + if currLibretranslateUrl: + setConfigParam(baseDir, + 'libretranslateUrl', '') + + # libretranslate API Key + currLibretranslateApiKey = \ + getConfigParam(baseDir, + 'libretranslateApiKey') + if fields.get('libretranslateApiKey'): + if fields['libretranslateApiKey'] != \ + currLibretranslateApiKey: + ltApiKey = fields['libretranslateApiKey'] + setConfigParam(baseDir, + 'libretranslateApiKey', + ltApiKey) + else: + if currLibretranslateApiKey: + setConfigParam(baseDir, + 'libretranslateApiKey', '') + + # change instance short description currInstanceDescriptionShort = \ getConfigParam(baseDir, 'instanceDescriptionShort') @@ -4432,6 +4675,8 @@ class PubServer(BaseHTTPRequestHandler): if currInstanceDescriptionShort: setConfigParam(baseDir, 'instanceDescriptionShort', '') + + # change instance description currInstanceDescription = \ getConfigParam(baseDir, 'instanceDescription') if fields.get('instanceDescription'): @@ -4504,6 +4749,18 @@ class PubServer(BaseHTTPRequestHandler): setBlogAddress(actorJson, '') actorChanged = True + # change Languages address + currentShowLanguages = getActorLanguages(actorJson) + if fields.get('showLanguages'): + if fields['showLanguages'] != currentShowLanguages: + setActorLanguages(baseDir, actorJson, + fields['showLanguages']) + actorChanged = True + else: + if currentShowLanguages: + setActorLanguages(baseDir, actorJson, '') + actorChanged = True + # change tox address currentToxAddress = getToxAddress(actorJson) if fields.get('toxAddress'): @@ -4588,6 +4845,20 @@ class PubServer(BaseHTTPRequestHandler): setDonationUrl(actorJson, '') actorChanged = True + # change website + currentWebsite = \ + getWebsite(actorJson, self.server.translate) + if fields.get('websiteUrl'): + if fields['websiteUrl'] != currentWebsite: + setWebsite(actorJson, + fields['websiteUrl'], + self.server.translate) + actorChanged = True + else: + if currentWebsite: + setWebsite(actorJson, '', self.server.translate) + actorChanged = True + # account moved to new address movedTo = '' if actorJson.get('movedTo'): @@ -4723,6 +4994,51 @@ class PubServer(BaseHTTPRequestHandler): brochMode) setConfigParam(baseDir, "brochMode", brochMode) + # shared item federation domains + siDomainUpdated = False + sharedItemsFederatedDomainsStr = \ + getConfigParam(baseDir, + "sharedItemsFederatedDomains") + if not sharedItemsFederatedDomainsStr: + sharedItemsFederatedDomainsStr = '' + sharedItemsFormStr = '' + if fields.get('shareDomainList'): + sharedItemsList = \ + sharedItemsFederatedDomainsStr.split(',') + for sharedFederatedDomain in sharedItemsList: + sharedItemsFormStr += \ + sharedFederatedDomain.strip() + '\n' + + shareDomainList = fields['shareDomainList'] + if shareDomainList != \ + sharedItemsFormStr: + sharedItemsFormStr2 = \ + shareDomainList.replace('\n', ',') + sharedItemsField = \ + "sharedItemsFederatedDomains" + setConfigParam(baseDir, sharedItemsField, + sharedItemsFormStr2) + siDomainUpdated = True + else: + if sharedItemsFederatedDomainsStr: + sharedItemsField = \ + "sharedItemsFederatedDomains" + setConfigParam(baseDir, sharedItemsField, + '') + siDomainUpdated = True + if siDomainUpdated: + siDomains = sharedItemsFormStr.split('\n') + siTokens = \ + self.server.sharedItemFederationTokens + self.server.sharedItemsFederatedDomains = \ + siDomains + domainFull = self.server.domainFull + self.server.sharedItemFederationTokens = \ + mergeSharedItemTokens(self.server.baseDir, + domainFull, + siDomains, + siTokens) + # change moderators list if fields.get('moderators'): if path.startswith('/users/' + @@ -5092,8 +5408,11 @@ class PubServer(BaseHTTPRequestHandler): if fields.get('isGroup'): if fields['isGroup'] == 'on': if actorJson['type'] != 'Group': - actorJson['type'] = 'Group' - actorChanged = True + # only allow admin to create groups + if path.startswith('/users/' + + adminNickname + '/'): + actorJson['type'] = 'Group' + actorChanged = True else: # this account is a person (default) if actorJson['type'] != 'Person': @@ -5112,6 +5431,20 @@ class PubServer(BaseHTTPRequestHandler): else: disableGrayscale(baseDir) + # low bandwidth images checkbox + if path.startswith('/users/' + adminNickname + '/') or \ + isArtist(baseDir, nickname): + currLowBandwidth = \ + getConfigParam(baseDir, 'lowBandwidth') + lowBandwidth = False + if fields.get('lowBandwidth'): + if fields['lowBandwidth'] == 'on': + lowBandwidth = True + if currLowBandwidth != lowBandwidth: + setConfigParam(baseDir, 'lowBandwidth', + lowBandwidth) + self.server.lowBandwidth = lowBandwidth + # save filtered words list filterFilename = \ acctDir(baseDir, nickname, domain) + \ @@ -5411,7 +5744,7 @@ class PubServer(BaseHTTPRequestHandler): ensure_ascii=False).encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, - None, callingDomain) + None, callingDomain, False) self._write(msg) if self.server.debug: print('Sent manifest: ' + callingDomain) @@ -5460,7 +5793,8 @@ class PubServer(BaseHTTPRequestHandler): self._set_headers_etag(faviconFilename, favType, favBinary, None, - self.server.domainFull) + self.server.domainFull, + False, None) self._write(favBinary) if debug: print('Sent favicon from cache: ' + callingDomain) @@ -5472,7 +5806,8 @@ class PubServer(BaseHTTPRequestHandler): self._set_headers_etag(faviconFilename, favType, favBinary, None, - self.server.domainFull) + self.server.domainFull, + False, None) self._write(favBinary) self.server.iconsCache[favFilename] = favBinary if self.server.debug: @@ -5501,7 +5836,7 @@ class PubServer(BaseHTTPRequestHandler): ensure_ascii=False).encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, - None, callingDomain) + None, callingDomain, False) self._write(msg) def _getExportedTheme(self, callingDomain: str, path: str, @@ -5517,7 +5852,7 @@ class PubServer(BaseHTTPRequestHandler): exportType = 'application/zip' self._set_headers_etag(filename, exportType, exportBinary, None, - domainFull) + domainFull, False, None) self._write(exportBinary) self._404() @@ -5550,7 +5885,7 @@ class PubServer(BaseHTTPRequestHandler): self._set_headers_etag(fontFilename, fontType, fontBinary, None, - self.server.domainFull) + self.server.domainFull, False, None) self._write(fontBinary) if debug: print('font sent from cache: ' + @@ -5566,7 +5901,8 @@ class PubServer(BaseHTTPRequestHandler): self._set_headers_etag(fontFilename, fontType, fontBinary, None, - self.server.domainFull) + self.server.domainFull, + False, None) self._write(fontBinary) self.server.fontsCache[fontStr] = fontBinary if debug: @@ -5614,12 +5950,13 @@ class PubServer(BaseHTTPRequestHandler): domain, port, maxPostsInRSSFeed, 1, - True) + True, + self.server.systemLanguage) if msg is not None: msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/xml', msglen, - None, callingDomain) + None, callingDomain, True) self._write(msg) if debug: print('Sent rss2 feed: ' + @@ -5669,7 +6006,8 @@ class PubServer(BaseHTTPRequestHandler): domain, port, maxPostsInRSSFeed, 1, - False) + False, + self.server.systemLanguage) break if msg: msg = rss2Header(httpPrefix, @@ -5679,7 +6017,7 @@ class PubServer(BaseHTTPRequestHandler): msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/xml', msglen, - None, callingDomain) + None, callingDomain, True) self._write(msg) if debug: print('Sent rss2 feed: ' + @@ -5719,7 +6057,7 @@ class PubServer(BaseHTTPRequestHandler): msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/xml', msglen, - None, callingDomain) + None, callingDomain, True) self._write(msg) if debug: print('Sent rss2 newswire feed: ' + @@ -5755,7 +6093,7 @@ class PubServer(BaseHTTPRequestHandler): msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/xml', msglen, - None, callingDomain) + None, callingDomain, True) self._write(msg) if debug: print('Sent rss2 categories feed: ' + @@ -5771,7 +6109,7 @@ class PubServer(BaseHTTPRequestHandler): baseDir: str, httpPrefix: str, domain: str, port: int, proxyType: str, GETstartTime, GETtimings: {}, - debug: bool) -> None: + debug: bool, systemLanguage: str) -> None: """Returns an RSS3 feed """ nickname = path.split('/blog/')[1] @@ -5795,12 +6133,13 @@ class PubServer(BaseHTTPRequestHandler): baseDir, httpPrefix, self.server.translate, nickname, domain, port, - maxPostsInRSSFeed, 1) + maxPostsInRSSFeed, 1, + systemLanguage) if msg is not None: msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/plain; charset=utf-8', - msglen, None, callingDomain) + msglen, None, callingDomain, True) self._write(msg) if self.server.debug: print('Sent rss3 feed: ' + @@ -5846,6 +6185,7 @@ class PubServer(BaseHTTPRequestHandler): if len(optionsList) > 3: optionsLink = optionsList[3] donateUrl = None + websiteUrl = None PGPpubKey = None PGPfingerprint = None xmppAddress = None @@ -5869,6 +6209,7 @@ class PubServer(BaseHTTPRequestHandler): movedTo = actorJson['movedTo'] lockedAccount = getLockedAccount(actorJson) donateUrl = getDonationUrl(actorJson) + websiteUrl = getWebsite(actorJson, self.server.translate) xmppAddress = getXmppAddress(actorJson) matrixAddress = getMatrixAddress(actorJson) ssbAddress = getSSBAddress(actorJson) @@ -5907,7 +6248,7 @@ class PubServer(BaseHTTPRequestHandler): optionsActor, optionsProfileUrl, optionsLink, - pageNumber, donateUrl, + pageNumber, donateUrl, websiteUrl, xmppAddress, matrixAddress, ssbAddress, blogAddress, toxAddress, briarAddress, @@ -5924,7 +6265,7 @@ class PubServer(BaseHTTPRequestHandler): accessKeys).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self._benchmarkGETtimings(GETstartTime, GETtimings, 'registered devices done', @@ -5966,11 +6307,17 @@ class PubServer(BaseHTTPRequestHandler): mediaFileType = mediaFileMimeType(mediaFilename) + t = os.path.getmtime(mediaFilename) + lastModifiedTime = datetime.datetime.fromtimestamp(t) + lastModifiedTimeStr = \ + lastModifiedTime.strftime('%a, %d %b %Y %H:%M:%S GMT') + with open(mediaFilename, 'rb') as avFile: mediaBinary = avFile.read() self._set_headers_etag(mediaFilename, mediaFileType, mediaBinary, None, - self.server.domainFull) + callingDomain, True, + lastModifiedTimeStr) self._write(mediaBinary) self._benchmarkGETtimings(GETstartTime, GETtimings, 'show emoji done', @@ -5998,7 +6345,8 @@ class PubServer(BaseHTTPRequestHandler): self._set_headers_etag(emojiFilename, mediaImageType, mediaBinary, None, - self.server.domainFull) + self.server.domainFull, + False, None) self._write(mediaBinary) self._benchmarkGETtimings(GETstartTime, GETtimings, 'background shown done', @@ -6034,7 +6382,8 @@ class PubServer(BaseHTTPRequestHandler): self._set_headers_etag(mediaFilename, mimeTypeStr, mediaBinary, None, - self.server.domainFull) + self.server.domainFull, + False, None) self._write(mediaBinary) return else: @@ -6045,7 +6394,8 @@ class PubServer(BaseHTTPRequestHandler): self._set_headers_etag(mediaFilename, mimeType, mediaBinary, None, - self.server.domainFull) + self.server.domainFull, + False, None) self._write(mediaBinary) self.server.iconsCache[mediaStr] = mediaBinary self._benchmarkGETtimings(GETstartTime, GETtimings, @@ -6088,7 +6438,8 @@ class PubServer(BaseHTTPRequestHandler): self._set_headers_etag(mediaFilename, mimeType, mediaBinary, None, - self.server.domainFull) + self.server.domainFull, + False, None) self._write(mediaBinary) self._benchmarkGETtimings(GETstartTime, GETtimings, 'show files done', @@ -6113,7 +6464,8 @@ class PubServer(BaseHTTPRequestHandler): self._set_headers_etag(mediaFilename, mimeType, mediaBinary, None, - self.server.domainFull) + self.server.domainFull, + False, None) self._write(mediaBinary) self._benchmarkGETtimings(GETstartTime, GETtimings, 'icon shown done', @@ -6172,12 +6524,14 @@ class PubServer(BaseHTTPRequestHandler): self.server.showPublishedDateOnly, self.server.peertubeInstances, self.server.allowLocalNetworkAccess, - self.server.themeName) + self.server.themeName, + self.server.systemLanguage, + self.server.maxLikeCount) if hashtagStr: msg = hashtagStr.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) else: originPathStr = path.split('/tags/')[0] @@ -6227,12 +6581,13 @@ class PubServer(BaseHTTPRequestHandler): self.server.personCache, httpPrefix, self.server.projectVersion, - self.server.YTReplacementDomain) + self.server.YTReplacementDomain, + self.server.systemLanguage) if hashtagStr: msg = hashtagStr.encode('utf-8') msglen = len(msg) self._set_headers('text/xml', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) else: originPathStr = path.split('/tags/rss2/')[0] @@ -6308,8 +6663,8 @@ class PubServer(BaseHTTPRequestHandler): return self.server.actorRepeat = path.split('?actor=')[1] announceToStr = \ - httpPrefix + '://' + domainFull + '/users/' + \ - self.postToNickname + '/followers' + localActorUrl(httpPrefix, self.postToNickname, domainFull) + \ + '/followers' if not repeatPrivate: announceToStr = 'https://www.w3.org/ns/activitystreams#Public' announceJson = \ @@ -6714,8 +7069,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.GETbusy = False return likeActor = \ - httpPrefix + '://' + \ - domainFull + '/users/' + self.postToNickname + localActorUrl(httpPrefix, self.postToNickname, domainFull) actorLiked = path.split('?actor=')[1] if '?' in actorLiked: actorLiked = actorLiked.split('?')[0] @@ -6813,7 +7167,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.GETbusy = False return undoActor = \ - httpPrefix + '://' + domainFull + '/users/' + self.postToNickname + localActorUrl(httpPrefix, self.postToNickname, domainFull) actorLiked = path.split('?actor=')[1] if '?' in actorLiked: actorLiked = actorLiked.split('?')[0] @@ -6911,7 +7265,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.GETbusy = False return bookmarkActor = \ - httpPrefix + '://' + domainFull + '/users/' + self.postToNickname + localActorUrl(httpPrefix, self.postToNickname, domainFull) ccList = [] bookmark(self.server.recentPostsCache, self.server.session, @@ -6997,7 +7351,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.GETbusy = False return undoActor = \ - httpPrefix + '://' + domainFull + '/users/' + self.postToNickname + localActorUrl(httpPrefix, self.postToNickname, domainFull) ccList = [] undoBookmark(self.server.recentPostsCache, self.server.session, @@ -7112,11 +7466,13 @@ class PubServer(BaseHTTPRequestHandler): self.server.showPublishedDateOnly, self.server.peertubeInstances, self.server.allowLocalNetworkAccess, - self.server.themeName) + self.server.themeName, + self.server.systemLanguage, + self.server.maxLikeCount) if deleteStr: deleteStrLen = len(deleteStr) self._set_headers('text/html', deleteStrLen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(deleteStr.encode('utf-8')) self.server.GETbusy = False return @@ -7262,15 +7618,15 @@ class PubServer(BaseHTTPRequestHandler): 'https://www.w3.org/ns/activitystreams' firstStr = \ - httpPrefix + '://' + domainFull + '/users/' + nickname + \ + localActorUrl(httpPrefix, nickname, domainFull) + \ '/statuses/' + statusNumber + '/replies?page=true' idStr = \ - httpPrefix + '://' + domainFull + '/users/' + nickname + \ + localActorUrl(httpPrefix, nickname, domainFull) + \ '/statuses/' + statusNumber + '/replies' lastStr = \ - httpPrefix + '://' + domainFull + '/users/' + nickname + \ + localActorUrl(httpPrefix, nickname, domainFull) + \ '/statuses/' + statusNumber + '/replies?page=true' repliesJson = { @@ -7320,11 +7676,13 @@ class PubServer(BaseHTTPRequestHandler): self.server.showPublishedDateOnly, peertubeInstances, self.server.allowLocalNetworkAccess, - self.server.themeName) + self.server.themeName, + self.server.systemLanguage, + self.server.maxLikeCount) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) else: if self._fetchAuthenticated(): @@ -7333,7 +7691,7 @@ class PubServer(BaseHTTPRequestHandler): protocolStr = 'application/json' msglen = len(msg) self._set_headers(protocolStr, msglen, None, - callingDomain) + callingDomain, False) self._write(msg) else: self._404() @@ -7345,13 +7703,12 @@ class PubServer(BaseHTTPRequestHandler): contextStr = 'https://www.w3.org/ns/activitystreams' idStr = \ - httpPrefix + '://' + domainFull + \ - '/users/' + nickname + '/statuses/' + \ - statusNumber + '?page=true' + localActorUrl(httpPrefix, nickname, domainFull) + \ + '/statuses/' + statusNumber + '?page=true' partOfStr = \ - httpPrefix + '://' + domainFull + \ - '/users/' + nickname + '/statuses/' + statusNumber + localActorUrl(httpPrefix, nickname, domainFull) + \ + '/statuses/' + statusNumber repliesJson = { '@context': contextStr, @@ -7408,11 +7765,13 @@ class PubServer(BaseHTTPRequestHandler): self.server.showPublishedDateOnly, peertubeInstances, self.server.allowLocalNetworkAccess, - self.server.themeName) + self.server.themeName, + self.server.systemLanguage, + self.server.maxLikeCount) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self._benchmarkGETtimings(GETstartTime, GETtimings, @@ -7426,7 +7785,7 @@ class PubServer(BaseHTTPRequestHandler): protocolStr = 'application/json' msglen = len(msg) self._set_headers(protocolStr, msglen, - None, callingDomain) + None, callingDomain, False) self._write(msg) else: self._404() @@ -7506,12 +7865,16 @@ class PubServer(BaseHTTPRequestHandler): self.server.allowLocalNetworkAccess, self.server.textModeBanner, self.server.debug, - accessKeys, city, rolesList, + accessKeys, city, + self.server.systemLanguage, + self.server.maxLikeCount, + self.server.sharedItemsFederatedDomains, + rolesList, None, None) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self._benchmarkGETtimings(GETstartTime, GETtimings, 'post replies done', @@ -7524,7 +7887,7 @@ class PubServer(BaseHTTPRequestHandler): msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, - None, callingDomain) + None, callingDomain, False) self._write(msg) else: self._404() @@ -7581,6 +7944,8 @@ class PubServer(BaseHTTPRequestHandler): city = getSpoofedCity(self.server.city, baseDir, nickname, domain) + sharedItemsFederatedDomains = \ + self.server.sharedItemsFederatedDomains msg = \ htmlProfile(self.server.rssIconAtTop, self.server.cssCache, @@ -7604,12 +7969,16 @@ class PubServer(BaseHTTPRequestHandler): allowLocalNetworkAccess, self.server.textModeBanner, self.server.debug, - accessKeys, city, skills, + accessKeys, city, + self.server.systemLanguage, + self.server.maxLikeCount, + sharedItemsFederatedDomains, + skills, None, None) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self._benchmarkGETtimings(GETstartTime, GETtimings, @@ -7626,7 +7995,7 @@ class PubServer(BaseHTTPRequestHandler): msglen = len(msg) self._set_headers('application/json', msglen, None, - callingDomain) + callingDomain, False) self._write(msg) else: self._404() @@ -7737,11 +8106,13 @@ class PubServer(BaseHTTPRequestHandler): self.server.showPublishedDateOnly, self.server.peertubeInstances, self.server.allowLocalNetworkAccess, - self.server.themeName) + self.server.themeName, + self.server.systemLanguage, + self.server.maxLikeCount) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self._benchmarkGETtimings(GETstartTime, GETtimings, @@ -7756,7 +8127,7 @@ class PubServer(BaseHTTPRequestHandler): msglen = len(msg) self._set_headers('application/json', msglen, - None, callingDomain) + None, callingDomain, False) self._write(msg) else: self._404() @@ -7914,6 +8285,8 @@ class PubServer(BaseHTTPRequestHandler): accessKeys = \ self.server.keyShortcuts[nickname] + sharedItemsFederatedDomains = \ + self.server.sharedItemsFederatedDomains msg = htmlInbox(self.server.cssCache, defaultTimeline, recentPostsCache, @@ -7946,7 +8319,10 @@ class PubServer(BaseHTTPRequestHandler): self.server.peertubeInstances, self.server.allowLocalNetworkAccess, self.server.textModeBanner, - accessKeys) + accessKeys, + self.server.systemLanguage, + self.server.maxLikeCount, + sharedItemsFederatedDomains) if GETstartTime: self._benchmarkGETtimings(GETstartTime, GETtimings, 'show status done', @@ -7956,7 +8332,7 @@ class PubServer(BaseHTTPRequestHandler): msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) if GETstartTime: @@ -7970,7 +8346,7 @@ class PubServer(BaseHTTPRequestHandler): msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, - None, callingDomain) + None, callingDomain, False) self._write(msg) self.server.GETbusy = False return True @@ -8050,6 +8426,8 @@ class PubServer(BaseHTTPRequestHandler): accessKeys = \ self.server.keyShortcuts[nickname] + sharedItemsFederatedDomains = \ + self.server.sharedItemsFederatedDomains msg = \ htmlInboxDMs(self.server.cssCache, self.server.defaultTimeline, @@ -8082,11 +8460,14 @@ class PubServer(BaseHTTPRequestHandler): self.server.peertubeInstances, self.server.allowLocalNetworkAccess, self.server.textModeBanner, - accessKeys) + accessKeys, + self.server.systemLanguage, + self.server.maxLikeCount, + sharedItemsFederatedDomains) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self._benchmarkGETtimings(GETstartTime, GETtimings, 'show inbox done', @@ -8099,7 +8480,7 @@ class PubServer(BaseHTTPRequestHandler): msglen = len(msg) self._set_headers('application/json', msglen, - None, callingDomain) + None, callingDomain, False) self._write(msg) self.server.GETbusy = False return True @@ -8179,6 +8560,8 @@ class PubServer(BaseHTTPRequestHandler): accessKeys = \ self.server.keyShortcuts[nickname] + sharedItemsFederatedDomains = \ + self.server.sharedItemsFederatedDomains msg = \ htmlInboxReplies(self.server.cssCache, self.server.defaultTimeline, @@ -8211,11 +8594,14 @@ class PubServer(BaseHTTPRequestHandler): self.server.peertubeInstances, self.server.allowLocalNetworkAccess, self.server.textModeBanner, - accessKeys) + accessKeys, + self.server.systemLanguage, + self.server.maxLikeCount, + sharedItemsFederatedDomains) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self._benchmarkGETtimings(GETstartTime, GETtimings, 'show dms done', @@ -8228,7 +8614,7 @@ class PubServer(BaseHTTPRequestHandler): msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, - None, callingDomain) + None, callingDomain, False) self._write(msg) self.server.GETbusy = False return True @@ -8341,11 +8727,14 @@ class PubServer(BaseHTTPRequestHandler): self.server.peertubeInstances, self.server.allowLocalNetworkAccess, self.server.textModeBanner, - accessKeys) + accessKeys, + self.server.systemLanguage, + self.server.maxLikeCount, + self.server.sharedItemsFederatedDomains) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self._benchmarkGETtimings(GETstartTime, GETtimings, 'show replies 2 done', @@ -8358,7 +8747,7 @@ class PubServer(BaseHTTPRequestHandler): msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, - None, callingDomain) + None, callingDomain, False) self._write(msg) self.server.GETbusy = False return True @@ -8471,11 +8860,14 @@ class PubServer(BaseHTTPRequestHandler): self.server.peertubeInstances, self.server.allowLocalNetworkAccess, self.server.textModeBanner, - accessKeys) + accessKeys, + self.server.systemLanguage, + self.server.maxLikeCount, + self.server.sharedItemsFederatedDomains) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self._benchmarkGETtimings(GETstartTime, GETtimings, 'show media 2 done', @@ -8489,7 +8881,7 @@ class PubServer(BaseHTTPRequestHandler): msglen = len(msg) self._set_headers('application/json', msglen, - None, callingDomain) + None, callingDomain, False) self._write(msg) self.server.GETbusy = False return True @@ -8610,11 +9002,14 @@ class PubServer(BaseHTTPRequestHandler): self.server.peertubeInstances, self.server.allowLocalNetworkAccess, self.server.textModeBanner, - accessKeys) + accessKeys, + self.server.systemLanguage, + self.server.maxLikeCount, + self.server.sharedItemsFederatedDomains) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self._benchmarkGETtimings(GETstartTime, GETtimings, 'show blogs 2 done', @@ -8628,7 +9023,7 @@ class PubServer(BaseHTTPRequestHandler): msglen = len(msg) self._set_headers('application/json', msglen, - None, callingDomain) + None, callingDomain, False) self._write(msg) self.server.GETbusy = False return True @@ -8712,6 +9107,8 @@ class PubServer(BaseHTTPRequestHandler): accessKeys = \ self.server.keyShortcuts[nickname] + sharedItemsFederatedDomains = \ + self.server.sharedItemsFederatedDomains msg = \ htmlInboxFeatures(self.server.cssCache, self.server.defaultTimeline, @@ -8745,11 +9142,14 @@ class PubServer(BaseHTTPRequestHandler): self.server.peertubeInstances, self.server.allowLocalNetworkAccess, self.server.textModeBanner, - accessKeys) + accessKeys, + self.server.systemLanguage, + self.server.maxLikeCount, + sharedItemsFederatedDomains) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self._benchmarkGETtimings(GETstartTime, GETtimings, 'show blogs 2 done', @@ -8763,7 +9163,7 @@ class PubServer(BaseHTTPRequestHandler): msglen = len(msg) self._set_headers('application/json', msglen, - None, callingDomain) + None, callingDomain, False) self._write(msg) self.server.GETbusy = False return True @@ -8841,11 +9241,14 @@ class PubServer(BaseHTTPRequestHandler): self.server.peertubeInstances, self.server.allowLocalNetworkAccess, self.server.textModeBanner, - accessKeys) + accessKeys, + self.server.systemLanguage, + self.server.maxLikeCount, + self.server.sharedItemsFederatedDomains) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self._benchmarkGETtimings(GETstartTime, GETtimings, 'show blogs 2 done', @@ -8860,6 +9263,87 @@ class PubServer(BaseHTTPRequestHandler): self.server.GETbusy = False return True + def _showWantedTimeline(self, authorized: bool, + callingDomain: str, path: str, + baseDir: str, httpPrefix: str, + domain: str, domainFull: str, port: int, + onionDomain: str, i2pDomain: str, + GETstartTime, GETtimings: {}, + proxyType: str, cookie: str, + debug: str) -> bool: + """Shows the wanted timeline + """ + if '/users/' in path: + if authorized: + if self._requestHTTP(): + nickname = path.replace('/users/', '') + nickname = nickname.replace('/tlwanted', '') + pageNumber = 1 + if '?page=' in nickname: + pageNumber = nickname.split('?page=')[1] + nickname = nickname.split('?page=')[0] + if pageNumber.isdigit(): + pageNumber = int(pageNumber) + else: + pageNumber = 1 + + accessKeys = self.server.accessKeys + if self.server.keyShortcuts.get(nickname): + accessKeys = \ + self.server.keyShortcuts[nickname] + + msg = \ + htmlWanted(self.server.cssCache, + self.server.defaultTimeline, + self.server.recentPostsCache, + self.server.maxRecentPosts, + self.server.translate, + pageNumber, maxPostsInFeed, + self.server.session, + baseDir, + self.server.cachedWebfingers, + self.server.personCache, + nickname, + domain, + port, + self.server.allowDeletion, + httpPrefix, + self.server.projectVersion, + self.server.YTReplacementDomain, + self.server.showPublishedDateOnly, + self.server.newswire, + self.server.positiveVoting, + self.server.showPublishAsIcon, + self.server.fullWidthTimelineButtonHeader, + self.server.iconsAsButtons, + self.server.rssIconAtTop, + self.server.publishButtonAtTop, + authorized, self.server.themeName, + self.server.peertubeInstances, + self.server.allowLocalNetworkAccess, + self.server.textModeBanner, + accessKeys, + self.server.systemLanguage, + self.server.maxLikeCount, + self.server.sharedItemsFederatedDomains) + msg = msg.encode('utf-8') + msglen = len(msg) + self._set_headers('text/html', msglen, + cookie, callingDomain, False) + self._write(msg) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'show blogs 2 done', + 'show wanted 2') + self.server.GETbusy = False + return True + # not the shares timeline + if debug: + print('DEBUG: GET access to wanted timeline is unauthorized') + self.send_response(405) + self.end_headers() + self.server.GETbusy = False + return True + def _showBookmarksTimeline(self, authorized: bool, callingDomain: str, path: str, baseDir: str, httpPrefix: str, @@ -8921,6 +9405,8 @@ class PubServer(BaseHTTPRequestHandler): accessKeys = \ self.server.keyShortcuts[nickname] + sharedItemsFederatedDomains = \ + self.server.sharedItemsFederatedDomains msg = \ htmlBookmarks(self.server.cssCache, self.server.defaultTimeline, @@ -8954,11 +9440,14 @@ class PubServer(BaseHTTPRequestHandler): self.server.peertubeInstances, self.server.allowLocalNetworkAccess, self.server.textModeBanner, - accessKeys) + accessKeys, + self.server.systemLanguage, + self.server.maxLikeCount, + sharedItemsFederatedDomains) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self._benchmarkGETtimings(GETstartTime, GETtimings, 'show shares 2 done', @@ -8971,7 +9460,7 @@ class PubServer(BaseHTTPRequestHandler): msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, - None, callingDomain) + None, callingDomain, False) self._write(msg) self.server.GETbusy = False return True @@ -9003,41 +9492,44 @@ class PubServer(BaseHTTPRequestHandler): outboxFeed = \ personBoxJson(self.server.recentPostsCache, self.server.session, - baseDir, domain, - port, path, - httpPrefix, - maxPostsInFeed, 'outbox', + baseDir, domain, port, path, + httpPrefix, maxPostsInFeed, 'outbox', authorized, self.server.newswireVotesThreshold, self.server.positiveVoting, self.server.votingTimeMins) if outboxFeed: - if self._requestHTTP(): - nickname = \ - path.replace('/users/', '').replace('/outbox', '') + nickname = \ + path.replace('/users/', '').replace('/outbox', '') + pageNumber = 0 + if '?page=' in nickname: + pageNumber = nickname.split('?page=')[1] + nickname = nickname.split('?page=')[0] + if pageNumber.isdigit(): + pageNumber = int(pageNumber) + else: + pageNumber = 1 + else: + if self._requestHTTP(): + pageNumber = 1 + if authorized and pageNumber >= 1: + # if a page wasn't specified then show the first one + pageStr = '?page=' + str(pageNumber) + outboxFeed = \ + personBoxJson(self.server.recentPostsCache, + self.server.session, + baseDir, domain, port, + path + pageStr, + httpPrefix, + maxPostsInFeed, 'outbox', + authorized, + self.server.newswireVotesThreshold, + self.server.positiveVoting, + self.server.votingTimeMins) + else: pageNumber = 1 - if '?page=' in nickname: - pageNumber = nickname.split('?page=')[1] - nickname = nickname.split('?page=')[0] - if pageNumber.isdigit(): - pageNumber = int(pageNumber) - else: - pageNumber = 1 - if 'page=' not in path: - # if a page wasn't specified then show the first one - outboxFeed = \ - personBoxJson(self.server.recentPostsCache, - self.server.session, - baseDir, - domain, - port, - path + '?page=1', - httpPrefix, - maxPostsInFeed, 'outbox', - authorized, - self.server.newswireVotesThreshold, - self.server.positiveVoting, - self.server.votingTimeMins) + + if self._requestHTTP(): fullWidthTimelineButtonHeader = \ self.server.fullWidthTimelineButtonHeader minimalNick = isMinimal(baseDir, domain, nickname) @@ -9058,9 +9550,7 @@ class PubServer(BaseHTTPRequestHandler): baseDir, self.server.cachedWebfingers, self.server.personCache, - nickname, - domain, - port, + nickname, domain, port, outboxFeed, self.server.allowDeletion, httpPrefix, @@ -9080,11 +9570,14 @@ class PubServer(BaseHTTPRequestHandler): self.server.peertubeInstances, self.server.allowLocalNetworkAccess, self.server.textModeBanner, - accessKeys) + accessKeys, + self.server.systemLanguage, + self.server.maxLikeCount, + self.server.sharedItemsFederatedDomains) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self._benchmarkGETtimings(GETstartTime, GETtimings, 'show events done', @@ -9096,7 +9589,7 @@ class PubServer(BaseHTTPRequestHandler): msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, - None, callingDomain) + None, callingDomain, False) self._write(msg) else: self._404() @@ -9163,6 +9656,8 @@ class PubServer(BaseHTTPRequestHandler): accessKeys = \ self.server.keyShortcuts[nickname] + sharedItemsFederatedDomains = \ + self.server.sharedItemsFederatedDomains msg = \ htmlModeration(self.server.cssCache, self.server.defaultTimeline, @@ -9195,11 +9690,14 @@ class PubServer(BaseHTTPRequestHandler): self.server.peertubeInstances, self.server.allowLocalNetworkAccess, self.server.textModeBanner, - accessKeys) + accessKeys, + self.server.systemLanguage, + self.server.maxLikeCount, + sharedItemsFederatedDomains) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self._benchmarkGETtimings(GETstartTime, GETtimings, 'show outbox done', @@ -9212,7 +9710,7 @@ class PubServer(BaseHTTPRequestHandler): msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, - None, callingDomain) + None, callingDomain, False) self._write(msg) self.server.GETbusy = False return True @@ -9236,12 +9734,12 @@ class PubServer(BaseHTTPRequestHandler): onionDomain: str, i2pDomain: str, GETstartTime, GETtimings: {}, proxyType: str, cookie: str, - debug: str) -> bool: + debug: str, sharesFileType: str) -> bool: """Shows the shares feed """ shares = \ getSharesFeedForPerson(baseDir, domain, port, path, - httpPrefix, sharesPerPage) + httpPrefix, sharesFileType, sharesPerPage) if shares: if self._requestHTTP(): pageNumber = 1 @@ -9251,7 +9749,7 @@ class PubServer(BaseHTTPRequestHandler): shares = \ getSharesFeedForPerson(baseDir, domain, port, path + '?page=true', - httpPrefix, + httpPrefix, sharesFileType, sharesPerPage) else: pageNumberStr = path.split('?page=')[1] @@ -9262,7 +9760,7 @@ class PubServer(BaseHTTPRequestHandler): searchPath = path.split('?page=')[0] getPerson = \ personLookup(domain, - searchPath.replace('/shares', ''), + searchPath.replace('/' + sharesFileType, ''), baseDir) if getPerson: if not self.server.session: @@ -9297,7 +9795,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.projectVersion, baseDir, httpPrefix, authorized, - getPerson, 'shares', + getPerson, sharesFileType, self.server.session, self.server.cachedWebfingers, self.server.personCache, @@ -9311,12 +9809,15 @@ class PubServer(BaseHTTPRequestHandler): self.server.textModeBanner, self.server.debug, accessKeys, city, + self.server.systemLanguage, + self.server.maxLikeCount, + self.server.sharedItemsFederatedDomains, shares, pageNumber, sharesPerPage) msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self._benchmarkGETtimings(GETstartTime, GETtimings, 'show moderation done', @@ -9330,7 +9831,7 @@ class PubServer(BaseHTTPRequestHandler): msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, - None, callingDomain) + None, callingDomain, False) self._write(msg) else: self._404() @@ -9424,12 +9925,15 @@ class PubServer(BaseHTTPRequestHandler): self.server.textModeBanner, self.server.debug, accessKeys, city, + self.server.systemLanguage, + self.server.maxLikeCount, + self.server.sharedItemsFederatedDomains, following, pageNumber, followsPerPage).encode('utf-8') msglen = len(msg) self._set_headers('text/html', - msglen, cookie, callingDomain) + msglen, cookie, callingDomain, False) self._write(msg) self.server.GETbusy = False self._benchmarkGETtimings(GETstartTime, GETtimings, @@ -9442,7 +9946,7 @@ class PubServer(BaseHTTPRequestHandler): ensure_ascii=False).encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, - None, callingDomain) + None, callingDomain, False) self._write(msg) else: self._404() @@ -9537,12 +10041,15 @@ class PubServer(BaseHTTPRequestHandler): self.server.textModeBanner, self.server.debug, accessKeys, city, + self.server.systemLanguage, + self.server.maxLikeCount, + self.server.sharedItemsFederatedDomains, followers, pageNumber, followsPerPage).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self.server.GETbusy = False self._benchmarkGETtimings(GETstartTime, GETtimings, @@ -9555,7 +10062,7 @@ class PubServer(BaseHTTPRequestHandler): ensure_ascii=False).encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, - None, callingDomain) + None, callingDomain, False) self._write(msg) else: self._404() @@ -9568,18 +10075,18 @@ class PubServer(BaseHTTPRequestHandler): path: str, httpPrefix: str, nickname: str, domain: str, - domainFull: str): + domainFull: str, systemLanguage: str): """Returns the featured posts collections in actor/collections/featured """ featuredCollection = \ jsonPinPost(baseDir, httpPrefix, - nickname, domain, domainFull) + nickname, domain, domainFull, systemLanguage) msg = json.dumps(featuredCollection, ensure_ascii=False).encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, - None, callingDomain) + None, callingDomain, False) self._write(msg) def _getFeaturedTagsCollection(self, callingDomain: str, @@ -9607,7 +10114,7 @@ class PubServer(BaseHTTPRequestHandler): ensure_ascii=False).encode('utf-8') msglen = len(msg) self._set_headers('application/json', msglen, - None, callingDomain) + None, callingDomain, False) self._write(msg) def _showPersonProfile(self, authorized: bool, @@ -9673,21 +10180,32 @@ class PubServer(BaseHTTPRequestHandler): self.server.textModeBanner, self.server.debug, accessKeys, city, + self.server.systemLanguage, + self.server.maxLikeCount, + self.server.sharedItemsFederatedDomains, None, None).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self._benchmarkGETtimings(GETstartTime, GETtimings, 'show profile 4 done', 'show profile posts') else: if self._fetchAuthenticated(): + acceptStr = self.headers['Accept'] msgStr = json.dumps(actorJson, ensure_ascii=False) msg = msgStr.encode('utf-8') msglen = len(msg) - self._set_headers('application/ld+json', msglen, - cookie, callingDomain) + if 'application/ld+json' in acceptStr: + self._set_headers('application/ld+json', msglen, + cookie, callingDomain, False) + elif 'application/jrd+json' in acceptStr: + self._set_headers('application/jrd+json', msglen, + cookie, callingDomain, False) + else: + self._set_headers('application/activity+json', msglen, + cookie, callingDomain, False) self._write(msg) else: self._404() @@ -9738,12 +10256,14 @@ class PubServer(BaseHTTPRequestHandler): nickname, domain, port, maxPostsInBlogsFeed, pageNumber, - self.server.peertubeInstances) + self.server.peertubeInstances, + self.server.systemLanguage, + self.server.personCache) if msg is not None: msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self._benchmarkGETtimings(GETstartTime, GETtimings, 'blog view done', 'blog page') @@ -9778,6 +10298,7 @@ class PubServer(BaseHTTPRequestHandler): path.endswith('/followers') or \ path.endswith('/skills') or \ path.endswith('/roles') or \ + path.endswith('/wanted') or \ path.endswith('/shares'): divertToLoginScreen = False @@ -9833,7 +10354,7 @@ class PubServer(BaseHTTPRequestHandler): msg = css.encode('utf-8') msglen = len(msg) self._set_headers('text/css', msglen, - None, callingDomain) + None, callingDomain, False) self._write(msg) self._benchmarkGETtimings(GETstartTime, GETtimings, 'show login screen done', @@ -9872,7 +10393,8 @@ class PubServer(BaseHTTPRequestHandler): mimeType = mediaFileMimeType(qrFilename) self._set_headers_etag(qrFilename, mimeType, mediaBinary, None, - self.server.domainFull) + self.server.domainFull, + False, None) self._write(mediaBinary) self._benchmarkGETtimings(GETstartTime, GETtimings, 'login screen logo done', @@ -9911,7 +10433,8 @@ class PubServer(BaseHTTPRequestHandler): mimeType = mediaFileMimeType(bannerFilename) self._set_headers_etag(bannerFilename, mimeType, mediaBinary, None, - self.server.domainFull) + self.server.domainFull, + False, None) self._write(mediaBinary) self._benchmarkGETtimings(GETstartTime, GETtimings, 'account qrcode done', @@ -9952,7 +10475,8 @@ class PubServer(BaseHTTPRequestHandler): mimeType = mediaFileMimeType(bannerFilename) self._set_headers_etag(bannerFilename, mimeType, mediaBinary, None, - self.server.domainFull) + self.server.domainFull, + False, None) self._write(mediaBinary) self._benchmarkGETtimings(GETstartTime, GETtimings, 'account qrcode done', @@ -9998,7 +10522,8 @@ class PubServer(BaseHTTPRequestHandler): self._set_headers_etag(bgFilename, 'image/' + ext, bgBinary, None, - self.server.domainFull) + self.server.domainFull, + False, None) self._write(bgBinary) self._benchmarkGETtimings(GETstartTime, GETtimings, @@ -10019,8 +10544,7 @@ class PubServer(BaseHTTPRequestHandler): return True mediaStr = path.split('/sharefiles/')[1] - mediaFilename = \ - baseDir + '/sharefiles/' + mediaStr + mediaFilename = baseDir + '/sharefiles/' + mediaStr if not os.path.isfile(mediaFilename): self._404() return True @@ -10036,7 +10560,8 @@ class PubServer(BaseHTTPRequestHandler): self._set_headers_etag(mediaFilename, mediaFileType, mediaBinary, None, - self.server.domainFull) + self.server.domainFull, + False, None) self._write(mediaBinary) self._benchmarkGETtimings(GETstartTime, GETtimings, 'show media done', @@ -10086,13 +10611,19 @@ class PubServer(BaseHTTPRequestHandler): # The file has not changed self._304() return True + + t = os.path.getmtime(avatarFilename) + lastModifiedTime = datetime.datetime.fromtimestamp(t) + lastModifiedTimeStr = \ + lastModifiedTime.strftime('%a, %d %b %Y %H:%M:%S GMT') + mediaImageType = getImageMimeType(avatarFile) with open(avatarFilename, 'rb') as avFile: mediaBinary = avFile.read() - self._set_headers_etag(avatarFilename, - mediaImageType, + self._set_headers_etag(avatarFilename, mediaImageType, mediaBinary, None, - self.server.domainFull) + callingDomain, True, + lastModifiedTimeStr) self._write(mediaBinary) self._benchmarkGETtimings(GETstartTime, GETtimings, 'icon shown done', @@ -10152,7 +10683,7 @@ class PubServer(BaseHTTPRequestHandler): msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self.server.GETbusy = False return True @@ -10164,7 +10695,7 @@ class PubServer(BaseHTTPRequestHandler): shareDescription: str, replyPageNumber: int, domain: str, domainFull: str, GETstartTime, GETtimings: {}, cookie, - noDropDown: bool) -> bool: + noDropDown: bool, conversationId: str) -> bool: """Shows the new post screen """ isNewPostEndpoint = False @@ -10173,7 +10704,7 @@ class PubServer(BaseHTTPRequestHandler): newPostEnd = ('newpost', 'newblog', 'newunlisted', 'newfollowers', 'newdm', 'newreminder', 'newreport', 'newquestion', - 'newshare') + 'newshare', 'newwanted') for postType in newPostEnd: if path.endswith('/' + postType): isNewPostEndpoint = True @@ -10202,7 +10733,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.newswire, self.server.themeName, noDropDown, accessKeys, - customSubmitText).encode('utf-8') + customSubmitText, + conversationId).encode('utf-8') if not msg: print('Error replying to ' + inReplyToUrl) self._404() @@ -10210,7 +10742,7 @@ class PubServer(BaseHTTPRequestHandler): return True msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self.server.GETbusy = False self._benchmarkGETtimings(GETstartTime, GETtimings, @@ -10255,7 +10787,7 @@ class PubServer(BaseHTTPRequestHandler): if msg: msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) else: self._404() @@ -10289,7 +10821,7 @@ class PubServer(BaseHTTPRequestHandler): if msg: msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) else: self._404() @@ -10324,7 +10856,7 @@ class PubServer(BaseHTTPRequestHandler): if msg: msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) else: self._404() @@ -10348,18 +10880,19 @@ class PubServer(BaseHTTPRequestHandler): postId = path.split('/editnewspost=')[1] if '?' in postId: postId = postId.split('?')[0] - postUrl = httpPrefix + '://' + domainFull + \ - '/users/' + postActor + '/statuses/' + postId + postUrl = localActorUrl(httpPrefix, postActor, domainFull) + \ + '/statuses/' + postId path = path.split('/editnewspost=')[0] msg = htmlEditNewsPost(self.server.cssCache, translate, baseDir, path, domain, port, httpPrefix, - postUrl).encode('utf-8') + postUrl, + self.server.systemLanguage).encode('utf-8') if msg: msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) else: self._404() @@ -10387,9 +10920,35 @@ class PubServer(BaseHTTPRequestHandler): ensure_ascii=False).encode('utf-8') msglen = len(msg) self._set_headers('application/json', - msglen, None, callingDomain) + msglen, None, callingDomain, False) self._write(msg) + def _sendBlock(self, httpPrefix: str, + blockerNickname: str, blockerDomainFull: str, + blockingNickname: str, blockingDomainFull: str) -> bool: + if blockerDomainFull == blockingDomainFull: + if blockerNickname == blockingNickname: + # don't block self + return False + blockActor = \ + localActorUrl(httpPrefix, blockerNickname, blockerDomainFull) + toUrl = 'https://www.w3.org/ns/activitystreams#Public' + ccUrl = blockActor + '/followers' + + blockedUrl = \ + httpPrefix + '://' + blockingDomainFull + \ + '/@' + blockingNickname + blockJson = { + "@context": "https://www.w3.org/ns/activitystreams", + 'type': 'Block', + 'actor': blockActor, + 'object': blockedUrl, + 'to': [toUrl], + 'cc': [ccUrl] + } + self._postToOutbox(blockJson, self.server.projectVersion) + return True + def do_GET(self): callingDomain = self.server.domainFull if self.headers.get('Host'): @@ -10506,7 +11065,6 @@ class PubServer(BaseHTTPRequestHandler): self._benchmarkGETtimings(GETstartTime, GETtimings, 'show logout', 'get cookie') - # manifest for progressive web apps if '/manifest.json' in self.path: if self._hasAccept(callingDomain): if not self._requestHTTP(): @@ -10542,6 +11100,191 @@ class PubServer(BaseHTTPRequestHandler): self._benchmarkGETtimings(GETstartTime, GETtimings, 'show logout', 'isAuthorized') + # shared items catalog for this instance + # this is only accessible to instance members or to + # other instances which present an authorization token + if self.path.startswith('/catalog') or \ + (self.path.startswith('/users/') and '/catalog' in self.path): + catalogAuthorized = authorized + if not catalogAuthorized: + if self.server.debug: + print('Catalog access is not authorized. ' + + 'Checking Authorization header') + # Check the authorization token + if self.headers.get('Origin') and \ + self.headers.get('Authorization'): + permittedDomains = \ + self.server.sharedItemsFederatedDomains + sharedItemTokens = self.server.sharedItemFederationTokens + if authorizeSharedItems(permittedDomains, + self.server.baseDir, + self.headers['Origin'], + callingDomain, + self.headers['Authorization'], + self.server.debug, + sharedItemTokens): + catalogAuthorized = True + elif self.server.debug: + print('Authorization token refused for ' + + 'shared items federation') + elif self.server.debug: + print('No Authorization header is available for ' + + 'shared items federation') + # show shared items catalog for federation + if self._hasAccept(callingDomain) and catalogAuthorized: + catalogType = 'json' + if self.path.endswith('.csv') or self._requestCSV(): + catalogType = 'csv' + elif self.path.endswith('.json') or not self._requestHTTP(): + catalogType = 'json' + if self.server.debug: + print('Preparing DFC catalog in format ' + catalogType) + + if catalogType == 'json': + # catalog as a json + if not self.path.startswith('/users/'): + if self.server.debug: + print('Catalog for the instance') + catalogJson = \ + sharesCatalogEndpoint(self.server.baseDir, + self.server.httpPrefix, + self.server.domainFull, + self.path, 'shares') + else: + domainFull = self.server.domainFull + httpPrefix = self.server.httpPrefix + nickname = self.path.split('/users/')[1] + if '/' in nickname: + nickname = nickname.split('/')[0] + if self.server.debug: + print('Catalog for account: ' + nickname) + catalogJson = \ + sharesCatalogAccountEndpoint(self.server.baseDir, + httpPrefix, + nickname, + self.server.domain, + domainFull, + self.path, + self.server.debug, + 'shares') + msg = json.dumps(catalogJson, + ensure_ascii=False).encode('utf-8') + msglen = len(msg) + self._set_headers('application/json', + msglen, None, callingDomain, False) + self._write(msg) + return + elif catalogType == 'csv': + # catalog as a CSV file for import into a spreadsheet + msg = \ + sharesCatalogCSVEndpoint(self.server.baseDir, + self.server.httpPrefix, + self.server.domainFull, + self.path, + 'shares').encode('utf-8') + msglen = len(msg) + self._set_headers('text/csv', + msglen, None, callingDomain, False) + self._write(msg) + return + self._404() + return + self._400() + return + + # wanted items catalog for this instance + # this is only accessible to instance members or to + # other instances which present an authorization token + if self.path.startswith('/wantedItems') or \ + (self.path.startswith('/users/') and '/wantedItems' in self.path): + catalogAuthorized = authorized + if not catalogAuthorized: + if self.server.debug: + print('Wanted catalog access is not authorized. ' + + 'Checking Authorization header') + # Check the authorization token + if self.headers.get('Origin') and \ + self.headers.get('Authorization'): + permittedDomains = \ + self.server.sharedItemsFederatedDomains + sharedItemTokens = self.server.sharedItemFederationTokens + if authorizeSharedItems(permittedDomains, + self.server.baseDir, + self.headers['Origin'], + callingDomain, + self.headers['Authorization'], + self.server.debug, + sharedItemTokens): + catalogAuthorized = True + elif self.server.debug: + print('Authorization token refused for ' + + 'wanted items federation') + elif self.server.debug: + print('No Authorization header is available for ' + + 'wanted items federation') + # show wanted items catalog for federation + if self._hasAccept(callingDomain) and catalogAuthorized: + catalogType = 'json' + if self.path.endswith('.csv') or self._requestCSV(): + catalogType = 'csv' + elif self.path.endswith('.json') or not self._requestHTTP(): + catalogType = 'json' + if self.server.debug: + print('Preparing DFC wanted catalog in format ' + + catalogType) + + if catalogType == 'json': + # catalog as a json + if not self.path.startswith('/users/'): + if self.server.debug: + print('Wanted catalog for the instance') + catalogJson = \ + sharesCatalogEndpoint(self.server.baseDir, + self.server.httpPrefix, + self.server.domainFull, + self.path, 'wanted') + else: + domainFull = self.server.domainFull + httpPrefix = self.server.httpPrefix + nickname = self.path.split('/users/')[1] + if '/' in nickname: + nickname = nickname.split('/')[0] + if self.server.debug: + print('Wanted catalog for account: ' + nickname) + catalogJson = \ + sharesCatalogAccountEndpoint(self.server.baseDir, + httpPrefix, + nickname, + self.server.domain, + domainFull, + self.path, + self.server.debug, + 'wanted') + msg = json.dumps(catalogJson, + ensure_ascii=False).encode('utf-8') + msglen = len(msg) + self._set_headers('application/json', + msglen, None, callingDomain, False) + self._write(msg) + return + elif catalogType == 'csv': + # catalog as a CSV file for import into a spreadsheet + msg = \ + sharesCatalogCSVEndpoint(self.server.baseDir, + self.server.httpPrefix, + self.server.domainFull, + self.path, + 'wanted').encode('utf-8') + msglen = len(msg) + self._set_headers('text/csv', + msglen, None, callingDomain, False) + self._write(msg) + return + self._404() + return + self._400() + return + # minimal mastodon api if self._mastoApi(self.path, callingDomain, authorized, self.server.httpPrefix, @@ -10703,7 +11446,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.port, self.server.proxyType, GETstartTime, GETtimings, - self.server.debug) + self.server.debug, + self.server.systemLanguage) return usersInPath = False @@ -10764,7 +11508,7 @@ class PubServer(BaseHTTPRequestHandler): msg = xmlStr.encode('utf-8') msglen = len(msg) self._set_headers('application/xrd+xml', msglen, - None, callingDomain) + None, callingDomain, False) self._write(msg) return @@ -10794,7 +11538,8 @@ class PubServer(BaseHTTPRequestHandler): getPinnedPostAsJson(self.server.baseDir, self.server.httpPrefix, nickname, self.server.domain, - self.server.domainFull) + self.server.domainFull, + self.server.systemLanguage) messageJson = {} if pinnedPostJson: postId = pinnedPostJson['id'] @@ -10812,7 +11557,7 @@ class PubServer(BaseHTTPRequestHandler): ensure_ascii=False).encode('utf-8') msglen = len(msg) self._set_headers('application/json', - msglen, None, callingDomain) + msglen, None, callingDomain, False) self._write(msg) return @@ -10826,7 +11571,8 @@ class PubServer(BaseHTTPRequestHandler): self.path, self.server.httpPrefix, nickname, self.server.domain, - self.server.domainFull) + self.server.domainFull, + self.server.systemLanguage) return if not htmlGET and \ @@ -10863,12 +11609,14 @@ class PubServer(BaseHTTPRequestHandler): self.server.domain, self.server.port, maxPostsInBlogsFeed, - self.server.peertubeInstances) + self.server.peertubeInstances, + self.server.systemLanguage, + self.server.personCache) if msg is not None: msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self._benchmarkGETtimings(GETstartTime, GETtimings, 'rss3 done', 'blog view') @@ -10915,7 +11663,7 @@ class PubServer(BaseHTTPRequestHandler): msglen = len(msg) self._set_headers('application/json', msglen, - None, callingDomain) + None, callingDomain, False) self._write(msg) self._benchmarkGETtimings(GETstartTime, GETtimings, 'blog page', @@ -10962,12 +11710,13 @@ class PubServer(BaseHTTPRequestHandler): self.server.domainFull, postJsonObject, self.server.peertubeInstances, - self.server.systemLanguage) + self.server.systemLanguage, + self.server.personCache) if msg is not None: msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self._benchmarkGETtimings(GETstartTime, GETtimings, 'person options done', @@ -10980,19 +11729,20 @@ class PubServer(BaseHTTPRequestHandler): 'person options done', 'blog post 2 done') - # remove a shared item - if htmlGET and '?rmshare=' in self.path: - shareName = self.path.split('?rmshare=')[1] - shareName = urllib.parse.unquote_plus(shareName.strip()) - usersPath = self.path.split('?rmshare=')[0] - actor = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + usersPath - msg = htmlConfirmRemoveSharedItem(self.server.cssCache, - self.server.translate, - self.server.baseDir, - actor, shareName, - callingDomain).encode('utf-8') + # after selecting a shared item from the left column then show it + if htmlGET and '?showshare=' in self.path and '/users/' in self.path: + itemID = self.path.split('?showshare=')[1] + usersPath = self.path.split('?showshare=')[0] + nickname = usersPath.replace('/users/', '') + itemID = urllib.parse.unquote_plus(itemID.strip()) + msg = \ + htmlShowShare(self.server.baseDir, + self.server.domain, nickname, + self.server.httpPrefix, self.server.domainFull, + itemID, self.server.translate, + self.server.sharedItemsFederatedDomains, + self.server.defaultTimeline, + self.server.themeName, 'shares') if not msg: if callingDomain.endswith('.onion') and \ self.server.onionDomain: @@ -11003,9 +11753,110 @@ class PubServer(BaseHTTPRequestHandler): self._redirect_headers(actor + '/tlshares', cookie, callingDomain) return + msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) + self._write(msg) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'blog post 2 done', + 'htmlShowShare') + return + + # after selecting a wanted item from the left column then show it + if htmlGET and '?showwanted=' in self.path and '/users/' in self.path: + itemID = self.path.split('?showwanted=')[1] + usersPath = self.path.split('?showwanted=')[0] + nickname = usersPath.replace('/users/', '') + itemID = urllib.parse.unquote_plus(itemID.strip()) + msg = \ + htmlShowShare(self.server.baseDir, + self.server.domain, nickname, + self.server.httpPrefix, self.server.domainFull, + itemID, self.server.translate, + self.server.sharedItemsFederatedDomains, + self.server.defaultTimeline, + self.server.themeName, 'wanted') + if not msg: + if callingDomain.endswith('.onion') and \ + self.server.onionDomain: + actor = 'http://' + self.server.onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and + self.server.i2pDomain): + actor = 'http://' + self.server.i2pDomain + usersPath + self._redirect_headers(actor + '/tlwanted', + cookie, callingDomain) + return + msg = msg.encode('utf-8') + msglen = len(msg) + self._set_headers('text/html', msglen, + cookie, callingDomain, False) + self._write(msg) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'blog post 2 done', + 'htmlShowWanted') + return + + # remove a shared item + if htmlGET and '?rmshare=' in self.path: + itemID = self.path.split('?rmshare=')[1] + itemID = urllib.parse.unquote_plus(itemID.strip()) + usersPath = self.path.split('?rmshare=')[0] + actor = \ + self.server.httpPrefix + '://' + \ + self.server.domainFull + usersPath + msg = htmlConfirmRemoveSharedItem(self.server.cssCache, + self.server.translate, + self.server.baseDir, + actor, itemID, + callingDomain, 'shares') + if not msg: + if callingDomain.endswith('.onion') and \ + self.server.onionDomain: + actor = 'http://' + self.server.onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and + self.server.i2pDomain): + actor = 'http://' + self.server.i2pDomain + usersPath + self._redirect_headers(actor + '/tlshares', + cookie, callingDomain) + return + msg = msg.encode('utf-8') + msglen = len(msg) + self._set_headers('text/html', msglen, + cookie, callingDomain, False) + self._write(msg) + self._benchmarkGETtimings(GETstartTime, GETtimings, + 'blog post 2 done', + 'remove shared item') + return + + # remove a wanted item + if htmlGET and '?rmwanted=' in self.path: + itemID = self.path.split('?rmwanted=')[1] + itemID = urllib.parse.unquote_plus(itemID.strip()) + usersPath = self.path.split('?rmwanted=')[0] + actor = \ + self.server.httpPrefix + '://' + \ + self.server.domainFull + usersPath + msg = htmlConfirmRemoveSharedItem(self.server.cssCache, + self.server.translate, + self.server.baseDir, + actor, itemID, + callingDomain, 'wanted') + if not msg: + if callingDomain.endswith('.onion') and \ + self.server.onionDomain: + actor = 'http://' + self.server.onionDomain + usersPath + elif (callingDomain.endswith('.i2p') and + self.server.i2pDomain): + actor = 'http://' + self.server.i2pDomain + usersPath + self._redirect_headers(actor + '/tlwanted', + cookie, callingDomain) + return + msg = msg.encode('utf-8') + msglen = len(msg) + self._set_headers('text/html', msglen, + cookie, callingDomain, False) self._write(msg) self._benchmarkGETtimings(GETstartTime, GETtimings, 'blog post 2 done', @@ -11277,7 +12128,8 @@ class PubServer(BaseHTTPRequestHandler): mimeType = mediaFileMimeType(mediaFilename) self._set_headers_etag(mediaFilename, mimeType, mediaBinary, cookie, - self.server.domainFull) + self.server.domainFull, + False, None) self._write(mediaBinary) self._benchmarkGETtimings(GETstartTime, GETtimings, 'profile.css done', @@ -11318,7 +12170,8 @@ class PubServer(BaseHTTPRequestHandler): mimeType = mediaFileMimeType(screenFilename) self._set_headers_etag(screenFilename, mimeType, mediaBinary, cookie, - self.server.domainFull) + self.server.domainFull, + False, None) self._write(mediaBinary) self._benchmarkGETtimings(GETstartTime, GETtimings, 'manifest logo done', @@ -11360,7 +12213,8 @@ class PubServer(BaseHTTPRequestHandler): self._set_headers_etag(iconFilename, mimeTypeStr, mediaBinary, cookie, - self.server.domainFull) + self.server.domainFull, + False, None) self._write(mediaBinary) self._benchmarkGETtimings(GETstartTime, GETtimings, 'show screenshot done', @@ -11634,7 +12488,7 @@ class PubServer(BaseHTTPRequestHandler): accessKeys).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self.server.GETbusy = False return @@ -11657,6 +12511,8 @@ class PubServer(BaseHTTPRequestHandler): '/users/' + nickname + '/' + self.server.defaultTimeline iconsAsButtons = self.server.iconsAsButtons defaultTimeline = self.server.defaultTimeline + sharedItemsDomains = \ + self.server.sharedItemsFederatedDomains msg = htmlLinksMobile(self.server.cssCache, self.server.baseDir, nickname, self.server.domainFull, @@ -11668,9 +12524,11 @@ class PubServer(BaseHTTPRequestHandler): iconsAsButtons, defaultTimeline, self.server.themeName, - accessKeys).encode('utf-8') + accessKeys, + sharedItemsDomains).encode('utf-8') msglen = len(msg) - self._set_headers('text/html', msglen, cookie, callingDomain) + self._set_headers('text/html', msglen, cookie, callingDomain, + False) self._write(msg) self.server.GETbusy = False return @@ -11754,7 +12612,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.textModeBanner, accessKeys).encode('utf-8') msglen = len(msg) - self._set_headers('text/html', msglen, cookie, callingDomain) + self._set_headers('text/html', msglen, cookie, callingDomain, + False) self._write(msg) self.server.GETbusy = False self._benchmarkGETtimings(GETstartTime, GETtimings, @@ -11772,7 +12631,8 @@ class PubServer(BaseHTTPRequestHandler): if msg: msg = msg.encode('utf-8') msglen = len(msg) - self._set_headers('text/html', msglen, cookie, callingDomain) + self._set_headers('text/html', msglen, cookie, callingDomain, + False) self._write(msg) self.server.GETbusy = False self._benchmarkGETtimings(GETstartTime, GETtimings, @@ -11805,7 +12665,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.textModeBanner, accessKeys).encode('utf-8') msglen = len(msg) - self._set_headers('text/html', msglen, cookie, callingDomain) + self._set_headers('text/html', msglen, cookie, callingDomain, + False) self._write(msg) self.server.GETbusy = False self._benchmarkGETtimings(GETstartTime, GETtimings, @@ -11847,7 +12708,7 @@ class PubServer(BaseHTTPRequestHandler): self.path).encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self.server.GETbusy = False self._benchmarkGETtimings(GETstartTime, GETtimings, @@ -12123,8 +12984,13 @@ class PubServer(BaseHTTPRequestHandler): replyToList = [] replyPageNumber = 1 shareDescription = None + conversationId = None # replytoActor = None if htmlGET: + if '?conversationId=' in self.path: + conversationId = self.path.split('?conversationId=')[1] + if '?' in conversationId: + conversationId = conversationId.split('?')[0] # public reply if '?replyto=' in self.path: inReplyToUrl = self.path.split('?replyto=')[1] @@ -12224,8 +13090,8 @@ class PubServer(BaseHTTPRequestHandler): nickname = getNicknameFromActor(self.path.split('?')[0]) if nickname == actor: postUrl = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + '/users/' + nickname + \ + localActorUrl(self.server.httpPrefix, nickname, + self.server.domainFull) + \ '/statuses/' + messageId msg = htmlEditBlog(self.server.mediaInstance, self.server.translate, @@ -12234,12 +13100,12 @@ class PubServer(BaseHTTPRequestHandler): self.path, replyPageNumber, nickname, self.server.domain, - postUrl) + postUrl, self.server.systemLanguage) if msg: msg = msg.encode('utf-8') msglen = len(msg) self._set_headers('text/html', msglen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(msg) self.server.GETbusy = False return @@ -12296,7 +13162,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.domain, self.server.domainFull, GETstartTime, GETtimings, - cookie, noDropDown): + cookie, noDropDown, conversationId): return self._benchmarkGETtimings(GETstartTime, GETtimings, @@ -12580,6 +13446,22 @@ class PubServer(BaseHTTPRequestHandler): cookie, self.server.debug): return + # get the wanted items timeline for a given person + if self.path.endswith('/tlwanted') or '/tlwanted?page=' in self.path: + if self._showWantedTimeline(authorized, + callingDomain, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.port, + self.server.onionDomain, + self.server.i2pDomain, + GETstartTime, GETtimings, + self.server.proxyType, + cookie, self.server.debug): + return + self._benchmarkGETtimings(GETstartTime, GETtimings, 'show blogs 2 done', 'show shares 2 done') @@ -12612,7 +13494,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.domain, self.server.port, searchHandle, - self.server.debug) + self.server.debug, + self.server.systemLanguage) msg = msg.encode('utf-8') msglen = len(msg) self._login_headers('text/html', @@ -12646,7 +13529,8 @@ class PubServer(BaseHTTPRequestHandler): self.server.domain, self.server.port, searchHandle, - self.server.debug) + self.server.debug, + self.server.systemLanguage) msg = msg.encode('utf-8') msglen = len(msg) self._login_headers('text/html', @@ -12678,19 +13562,21 @@ class PubServer(BaseHTTPRequestHandler): 'show bookmarks 2 done') # outbox timeline - if self._showOutboxTimeline(authorized, - callingDomain, self.path, - self.server.baseDir, - self.server.httpPrefix, - self.server.domain, - self.server.domainFull, - self.server.port, - self.server.onionDomain, - self.server.i2pDomain, - GETstartTime, GETtimings, - self.server.proxyType, - cookie, self.server.debug): - return + if self.path.endswith('/outbox') or \ + '/outbox?page=' in self.path: + if self._showOutboxTimeline(authorized, + callingDomain, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.port, + self.server.onionDomain, + self.server.i2pDomain, + GETstartTime, GETtimings, + self.server.proxyType, + cookie, self.server.debug): + return self._benchmarkGETtimings(GETstartTime, GETtimings, 'show events done', @@ -12728,7 +13614,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.i2pDomain, GETstartTime, GETtimings, self.server.proxyType, - cookie, self.server.debug): + cookie, self.server.debug, 'shares'): return self._benchmarkGETtimings(GETstartTime, GETtimings, @@ -12821,7 +13707,7 @@ class PubServer(BaseHTTPRequestHandler): msglen = len(msg) self._set_headers('application/json', msglen, - None, callingDomain) + None, callingDomain, False) self._write(msg) self._benchmarkGETtimings(GETstartTime, GETtimings, 'authenticated fetch', @@ -12877,7 +13763,7 @@ class PubServer(BaseHTTPRequestHandler): else: with open(mediaFilename, 'rb') as avFile: mediaBinary = avFile.read() - etag = sha1(mediaBinary).hexdigest() # nosec + etag = md5(mediaBinary).hexdigest() # nosec try: with open(mediaTagFilename, 'w+') as etagFile: etagFile.write(etag) @@ -12886,7 +13772,7 @@ class PubServer(BaseHTTPRequestHandler): mediaFileType = mediaFileMimeType(checkPath) self._set_headers_head(mediaFileType, fileLength, - etag, callingDomain) + etag, callingDomain, False) def _receiveNewPostProcess(self, postType: str, path: str, headers: {}, length: int, postBytes, boundary: str, @@ -12964,6 +13850,8 @@ class PubServer(BaseHTTPRequestHandler): city = getSpoofedCity(self.server.city, self.server.baseDir, nickname, self.server.domain) + if self.server.lowBandwidth: + convertImageToLowBandwidth(filename) processMetaData(self.server.baseDir, nickname, self.server.domain, filename, postImageFilename, city) @@ -13071,6 +13959,9 @@ class PubServer(BaseHTTPRequestHandler): city = getSpoofedCity(self.server.city, self.server.baseDir, nickname, self.server.domain) + conversationId = None + if fields.get('conversationId'): + conversationId = fields['conversationId'] messageJson = \ createPublicPost(self.server.baseDir, nickname, @@ -13085,14 +13976,19 @@ class PubServer(BaseHTTPRequestHandler): fields['replyTo'], fields['replyTo'], fields['subject'], fields['schedulePost'], fields['eventDate'], fields['eventTime'], - fields['location'], False) + fields['location'], False, + self.server.systemLanguage, + conversationId, + self.server.lowBandwidth) if messageJson: if fields['schedulePost']: return 1 if pinToProfile: + contentStr = \ + getBaseContentFromPost(messageJson, + self.server.systemLanguage) pinPost(self.server.baseDir, - nickname, self.server.domain, - messageJson['object']['content']) + nickname, self.server.domain, contentStr) return 1 if self._postToOutbox(messageJson, __version__, nickname): populateReplies(self.server.baseDir, @@ -13126,7 +14022,7 @@ class PubServer(BaseHTTPRequestHandler): messageJsonLen = len(messageJson) self._set_headers('text/html', messageJsonLen, - cookie, callingDomain) + cookie, callingDomain, False) self._write(messageJson) return 1 else: @@ -13142,6 +14038,9 @@ class PubServer(BaseHTTPRequestHandler): saveToFile = False clientToServer = False city = None + conversationId = None + if fields.get('conversationId'): + conversationId = fields['conversationId'] messageJson = \ createBlogPost(self.server.baseDir, nickname, self.server.domain, self.server.port, @@ -13157,7 +14056,10 @@ class PubServer(BaseHTTPRequestHandler): fields['schedulePost'], fields['eventDate'], fields['eventTime'], - fields['location']) + fields['location'], + self.server.systemLanguage, + conversationId, + self.server.lowBandwidth) if messageJson: if fields['schedulePost']: return 1 @@ -13218,6 +14120,9 @@ class PubServer(BaseHTTPRequestHandler): tags, 'content') postJsonObject['object']['content'] = fields['message'] + contentMap = postJsonObject['object']['contentMap'] + contentMap[self.server.systemLanguage] = \ + fields['message'] imgDescription = '' if fields.get('imageDescription'): @@ -13238,10 +14143,12 @@ class PubServer(BaseHTTPRequestHandler): filename, attachmentMediaType, imgDescription, - city) + city, + self.server.lowBandwidth) replaceYouTube(postJsonObject, - self.server.YTReplacementDomain) + self.server.YTReplacementDomain, + self.server.systemLanguage) saveJson(postJsonObject, postFilename) # also save to the news actor if nickname != 'news': @@ -13267,6 +14174,11 @@ class PubServer(BaseHTTPRequestHandler): followersOnly = False saveToFile = False clientToServer = False + + conversationId = None + if fields.get('conversationId'): + conversationId = fields['conversationId'] + messageJson = \ createUnlistedPost(self.server.baseDir, nickname, @@ -13284,7 +14196,10 @@ class PubServer(BaseHTTPRequestHandler): fields['schedulePost'], fields['eventDate'], fields['eventTime'], - fields['location']) + fields['location'], + self.server.systemLanguage, + conversationId, + self.server.lowBandwidth) if messageJson: if fields['schedulePost']: return 1 @@ -13306,6 +14221,11 @@ class PubServer(BaseHTTPRequestHandler): followersOnly = True saveToFile = False clientToServer = False + + conversationId = None + if fields.get('conversationId'): + conversationId = fields['conversationId'] + messageJson = \ createFollowersOnlyPost(self.server.baseDir, nickname, @@ -13325,7 +14245,10 @@ class PubServer(BaseHTTPRequestHandler): fields['schedulePost'], fields['eventDate'], fields['eventTime'], - fields['location']) + fields['location'], + self.server.systemLanguage, + conversationId, + self.server.lowBandwidth) if messageJson: if fields['schedulePost']: return 1 @@ -13350,6 +14273,11 @@ class PubServer(BaseHTTPRequestHandler): followersOnly = True saveToFile = False clientToServer = False + + conversationId = None + if fields.get('conversationId'): + conversationId = fields['conversationId'] + messageJson = \ createDirectMessagePost(self.server.baseDir, nickname, @@ -13370,7 +14298,10 @@ class PubServer(BaseHTTPRequestHandler): True, fields['schedulePost'], fields['eventDate'], fields['eventTime'], - fields['location']) + fields['location'], + self.server.systemLanguage, + conversationId, + self.server.lowBandwidth) if messageJson: if fields['schedulePost']: return 1 @@ -13400,6 +14331,7 @@ class PubServer(BaseHTTPRequestHandler): saveToFile = False clientToServer = False commentsEnabled = False + conversationId = None messageJson = \ createDirectMessagePost(self.server.baseDir, nickname, @@ -13417,7 +14349,10 @@ class PubServer(BaseHTTPRequestHandler): True, fields['schedulePost'], fields['eventDate'], fields['eventTime'], - fields['location']) + fields['location'], + self.server.systemLanguage, + conversationId, + self.server.lowBandwidth) if messageJson: if fields['schedulePost']: return 1 @@ -13449,7 +14384,9 @@ class PubServer(BaseHTTPRequestHandler): filename, attachmentMediaType, fields['imageDescription'], city, - self.server.debug, fields['subject']) + self.server.debug, fields['subject'], + self.server.systemLanguage, + self.server.lowBandwidth) if messageJson: if self._postToOutbox(messageJson, __version__, nickname): return 1 @@ -13472,6 +14409,7 @@ class PubServer(BaseHTTPRequestHandler): self.server.baseDir, nickname, self.server.domain) + intDuration = int(fields['duration']) messageJson = \ createQuestionPost(self.server.baseDir, nickname, @@ -13485,24 +14423,37 @@ class PubServer(BaseHTTPRequestHandler): fields['imageDescription'], city, fields['subject'], - int(fields['duration'])) + intDuration, + self.server.systemLanguage, + self.server.lowBandwidth) if messageJson: if self.server.debug: print('DEBUG: new Question') if self._postToOutbox(messageJson, __version__, nickname): return 1 return -1 - elif postType == 'newshare': + elif postType == 'newshare' or postType == 'newwanted': + if not fields.get('itemQty'): + print(postType + ' no itemQty') + return -1 if not fields.get('itemType'): + print(postType + ' no itemType') + return -1 + if 'itemPrice' not in fields: + print(postType + ' no itemPrice') + return -1 + if 'itemCurrency' not in fields: + print(postType + ' no itemCurrency') return -1 if not fields.get('category'): - return -1 - if not fields.get('location'): + print(postType + ' no category') return -1 if not fields.get('duration'): + print(postType + ' no duratio') return -1 if attachmentMediaType: if attachmentMediaType != 'image': + print('Attached media is not an image') return -1 durationStr = fields['duration'] if durationStr: @@ -13512,6 +14463,23 @@ class PubServer(BaseHTTPRequestHandler): self.server.baseDir, nickname, self.server.domain) + itemQty = 1 + if fields['itemQty']: + if isfloat(fields['itemQty']): + itemQty = float(fields['itemQty']) + itemPrice = "0.00" + itemCurrency = "EUR" + if fields['itemPrice']: + itemPrice, itemCurrency = \ + getPriceFromString(fields['itemPrice']) + if fields['itemCurrency']: + itemCurrency = fields['itemCurrency'] + if postType == 'newshare': + print('Adding shared item') + sharesFileType = 'shares' + else: + print('Adding wanted item') + sharesFileType = 'wanted' addShare(self.server.baseDir, self.server.httpPrefix, nickname, @@ -13519,12 +14487,15 @@ class PubServer(BaseHTTPRequestHandler): fields['subject'], fields['message'], filename, - fields['itemType'], + itemQty, fields['itemType'], fields['category'], fields['location'], durationStr, self.server.debug, - city) + city, itemPrice, itemCurrency, + self.server.systemLanguage, + self.server.translate, sharesFileType, + self.server.lowBandwidth) if filename: if os.path.isfile(filename): os.remove(filename) @@ -13738,7 +14709,7 @@ class PubServer(BaseHTTPRequestHandler): msglen = len(msg) self._set_headers('application/json', msglen, - None, callingDomain) + None, callingDomain, False) self._write(msg) return True return False @@ -13852,6 +14823,7 @@ class PubServer(BaseHTTPRequestHandler): self.path = self.path.replace('/tlblogs/', '/tlblogs') self.path = self.path.replace('/inbox/', '/inbox') self.path = self.path.replace('/shares/', '/shares') + self.path = self.path.replace('/wanted/', '/wanted') self.path = self.path.replace('/sharedInbox/', '/sharedInbox') if self.path == '/inbox': @@ -14052,6 +15024,19 @@ class PubServer(BaseHTTPRequestHandler): self.server.debug) return + # removes a wanted item + if self.path.endswith('/rmwanted'): + self._removeWanted(callingDomain, cookie, + authorized, self.path, + self.server.baseDir, + self.server.httpPrefix, + self.server.domain, + self.server.domainFull, + self.server.onionDomain, + self.server.i2pDomain, + self.server.debug) + return + self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 8) # removes a post @@ -14182,12 +15167,55 @@ class PubServer(BaseHTTPRequestHandler): self.server.defaultTimeline) return + # update the shared item federation token for the calling domain + # if it is within the permitted federation + if self.headers.get('Origin') and \ + self.headers.get('SharesCatalog'): + if self.server.debug: + print('SharesCatalog header: ' + self.headers['SharesCatalog']) + if not self.server.sharedItemsFederatedDomains: + siDomainsStr = getConfigParam(self.server.baseDir, + 'sharedItemsFederatedDomains') + if siDomainsStr: + if self.server.debug: + print('Loading shared items federated domains list') + siDomainsList = siDomainsStr.split(',') + domainsList = self.server.sharedItemsFederatedDomains + for siDomain in siDomainsList: + domainsList.append(siDomain.strip()) + originDomain = self.headers.get('Origin') + if originDomain != self.server.domainFull and \ + originDomain != self.server.onionDomain and \ + originDomain != self.server.i2pDomain and \ + originDomain in self.server.sharedItemsFederatedDomains: + if self.server.debug: + print('DEBUG: ' + + 'POST updating shared item federation ' + + 'token for ' + originDomain + ' to ' + + self.server.domainFull) + sharedItemTokens = self.server.sharedItemFederationTokens + sharesToken = self.headers['SharesCatalog'] + self.server.sharedItemFederationTokens = \ + updateSharedItemFederationToken(self.server.baseDir, + originDomain, + sharesToken, + self.server.debug, + sharedItemTokens) + elif self.server.debug: + if originDomain not in self.server.sharedItemsFederatedDomains: + print('originDomain is not in federated domains list ' + + originDomain) + else: + print('originDomain is not a different instance. ' + + originDomain + ' ' + self.server.domainFull + ' ' + + str(self.server.sharedItemsFederatedDomains)) + self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 14) # receive different types of post created by htmlNewPost postTypes = ("newpost", "newblog", "newunlisted", "newfollowers", - "newdm", "newreport", "newshare", "newquestion", - "editblogpost", "newreminder") + "newdm", "newreport", "newshare", "newwanted", + "newquestion", "editblogpost", "newreminder") for currPostType in postTypes: if not authorized: if self.server.debug: @@ -14196,7 +15224,9 @@ class PubServer(BaseHTTPRequestHandler): postRedirect = self.server.defaultTimeline if currPostType == 'newshare': - postRedirect = 'shares' + postRedirect = 'tlshares' + elif currPostType == 'newwanted': + postRedirect = 'tlwanted' pageNumber = \ self._receiveNewPost(currPostType, self.path, @@ -14213,23 +15243,25 @@ class PubServer(BaseHTTPRequestHandler): if callingDomain.endswith('.onion') and \ self.server.onionDomain: actorPathStr = \ - 'http://' + self.server.onionDomain + \ - '/users/' + nickname + '/' + postRedirect + \ + localActorUrl('http', nickname, + self.server.onionDomain) + \ + '/' + postRedirect + \ '?page=' + str(pageNumber) self._redirect_headers(actorPathStr, cookie, callingDomain) elif (callingDomain.endswith('.i2p') and self.server.i2pDomain): actorPathStr = \ - 'http://' + self.server.i2pDomain + \ - '/users/' + nickname + '/' + postRedirect + \ + localActorUrl('http', nickname, + self.server.i2pDomain) + \ + '/' + postRedirect + \ '?page=' + str(pageNumber) self._redirect_headers(actorPathStr, cookie, callingDomain) else: actorPathStr = \ - self.server.httpPrefix + '://' + \ - self.server.domainFull + '/users/' + nickname + \ + localActorUrl(self.server.httpPrefix, nickname, + self.server.domainFull) + \ '/' + postRedirect + '?page=' + str(pageNumber) self._redirect_headers(actorPathStr, cookie, callingDomain) @@ -14238,7 +15270,9 @@ class PubServer(BaseHTTPRequestHandler): self._benchmarkPOSTtimings(POSTstartTime, POSTtimings, 15) - if self.path.endswith('/outbox') or self.path.endswith('/shares'): + if self.path.endswith('/outbox') or \ + self.path.endswith('/wanted') or \ + self.path.endswith('/shares'): if usersInPath: if authorized: self.outboxAuthenticated = True @@ -14255,6 +15289,7 @@ class PubServer(BaseHTTPRequestHandler): # check that the post is to an expected path if not (self.path.endswith('/outbox') or self.path.endswith('/inbox') or + self.path.endswith('/wanted') or self.path.endswith('/shares') or self.path.endswith('/moderationaction') or self.path == '/sharedInbox'): @@ -14590,7 +15625,10 @@ def loadTokens(baseDir: str, tokensDict: {}, tokensLookup: {}) -> None: break -def runDaemon(userAgentsBlocked: [], +def runDaemon(lowBandwidth: bool, + maxLikeCount: int, + sharedItemsFederatedDomains: [], + userAgentsBlocked: [], logLoginFailures: bool, city: str, showNodeInfoAccounts: bool, @@ -14700,8 +15738,9 @@ def runDaemon(userAgentsBlocked: [], 'menuOutbox': 's', 'menuBookmarks': 'q', 'menuShares': 'h', + 'menuWanted': 'w', 'menuBlogs': 'b', - 'menuNewswire': 'w', + 'menuNewswire': 'u', 'menuLinks': 'l', 'menuMedia': 'm', 'menuModeration': 'o', @@ -14717,6 +15756,9 @@ def runDaemon(userAgentsBlocked: [], httpd.keyShortcuts = {} loadAccessKeysForAccounts(baseDir, httpd.keyShortcuts, httpd.accessKeys) + # wheither to use low bandwidth images + httpd.lowBandwidth = lowBandwidth + # list of blocked user agent types within the User-Agent header httpd.userAgentsBlocked = userAgentsBlocked @@ -14754,6 +15796,9 @@ def runDaemon(userAgentsBlocked: [], if not unitTest: httpd.translate, httpd.systemLanguage = \ loadTranslationsFromFile(baseDir, language) + if not httpd.systemLanguage: + print('ERROR: no system language loaded') + sys.exit() print('System language: ' + httpd.systemLanguage) if not httpd.translate: print('ERROR: no translations were loaded') @@ -14825,6 +15870,13 @@ def runDaemon(userAgentsBlocked: [], # for it to be considered dormant? httpd.dormantMonths = dormantMonths + # maximum number of likes to display on a post + httpd.maxLikeCount = maxLikeCount + if httpd.maxLikeCount < 0: + httpd.maxLikeCount = 0 + elif httpd.maxLikeCount > 16: + httpd.maxLikeCount = 16 + httpd.followingItemsPerPage = 12 if registration == 'open': httpd.registration = True @@ -14849,6 +15901,7 @@ def runDaemon(userAgentsBlocked: [], httpd.httpPrefix = httpPrefix httpd.debug = debug httpd.federationList = fedList.copy() + httpd.sharedItemsFederatedDomains = sharedItemsFederatedDomains.copy() httpd.baseDir = baseDir httpd.instanceId = instanceId httpd.personCache = {} @@ -14945,6 +15998,10 @@ def runDaemon(userAgentsBlocked: [], print('Creating archive') os.mkdir(archiveDir) + if not os.path.isdir(baseDir + '/sharefiles'): + print('Creating shared item files directory') + os.mkdir(baseDir + '/sharefiles') + print('Creating cache expiry thread') httpd.thrCache = \ threadWithTrace(target=expireCache, @@ -14987,6 +16044,14 @@ def runDaemon(userAgentsBlocked: [], httpd.iconsCache = {} httpd.fontsCache = {} + # create tokens used for shared item federation + httpd.sharedItemFederationTokens = \ + generateSharedItemFederationTokens(httpd.sharedItemsFederatedDomains, + baseDir) + httpd.sharedItemFederationTokens = \ + createSharedItemFederationToken(baseDir, httpd.domainFull, False, + httpd.sharedItemFederationTokens) + # load peertube instances from file into a list httpd.peertubeInstances = [] loadPeertubeInstances(baseDir, httpd.peertubeInstances) @@ -15013,7 +16078,9 @@ def runDaemon(userAgentsBlocked: [], httpd.allowLocalNetworkAccess, httpd.peertubeInstances, verifyAllSignatures, - httpd.themeName), daemon=True) + httpd.themeName, + httpd.systemLanguage, + httpd.maxLikeCount), daemon=True) print('Creating scheduled post thread') httpd.thrPostSchedule = \ @@ -15027,10 +16094,20 @@ def runDaemon(userAgentsBlocked: [], httpPrefix, domain, port, httpd.translate), daemon=True) + print('Creating federated shares thread') + httpd.thrFederatedSharesDaemon = \ + threadWithTrace(target=runFederatedSharesDaemon, + args=(baseDir, httpd, + httpPrefix, httpd.domainFull, + proxyType, debug, + httpd.systemLanguage), daemon=True) + # flags used when restarting the inbox queue httpd.restartInboxQueueInProgress = False httpd.restartInboxQueue = False + updateHashtagCategories(baseDir) + print('Adding hashtag categories for language ' + httpd.systemLanguage) loadHashtagCategories(baseDir, httpd.systemLanguage) @@ -15052,9 +16129,19 @@ def runDaemon(userAgentsBlocked: [], threadWithTrace(target=runNewswireWatchdog, args=(projectVersion, httpd), daemon=True) httpd.thrNewswireWatchdog.start() + + print('Creating federated shares watchdog') + httpd.thrFederatedSharesWatchdog = \ + threadWithTrace(target=runFederatedSharesWatchdog, + args=(projectVersion, httpd), daemon=True) + httpd.thrFederatedSharesWatchdog.start() else: + print('Starting inbox queue') httpd.thrInboxQueue.start() + print('Starting scheduled posts daemon') httpd.thrPostSchedule.start() + print('Starting federated shares daemon') + httpd.thrFederatedSharesDaemon.start() if clientToServer: print('Running ActivityPub client on ' + diff --git a/defaultcategories/en.xml b/defaultcategories/en.xml index 297b984f5..7dde26b01 100644 --- a/defaultcategories/en.xml +++ b/defaultcategories/en.xml @@ -4,663 +4,674 @@ #categories retro - retrocomputer kommunalwahl 90sretro A500 CreativeCommons ecommerce atarist SistersWithTransistors vax retroarch commodore retroffiting teletext Retromeme matariki floppy recommendation 8bit cassette arcade atari communicators atari800 oldschool trs80 communication atari8bit floppydisk retrocomputing recommended C64 nostalgia bbs ansi communicationtheory plan9 80s microcomputing kommunikation vaxvms retroarcade zdfretro woocommerce cassette_tapes bonhomme omm retrogaming z80 8bitdo retro atari800xl telekommunikation VollaCommunityDays retropie commodore64 cassettetapes retrogame Trans amiga bbcmicro retrofriday microcomputer bbsing commercial + retrocomputer kommunalwahl 90sretro A500 CreativeCommons ecommerce atarist SistersWithTransistors vax retroarch commodore retroffiting teletext Retromeme matariki floppy recommendation 8bit cassette arcade atari communicators atari800 oldschool trs80 communication atari8bit floppydisk retrocomputing recommended C64 nostalgia bbs ansi communicationtheory plan9 80s TransCrowdFund microcomputing kommunikation vaxvms retroarcade zdfretro cassette_tapes bonhomme omm acorn retrogaming z80 8bitdo retro atari800xl retrocom telekommunikation VollaCommunityDays retropie commodore64 cassettetapes retrogame Trans amiga bbcmicro retrofriday microcomputer bbsing commercial - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT economics - Europe workercoop InformationFriction cooperatives accounting bank bitcoin noplanetb theWorkshop feministeconomics WealthConcentration valueflows coops holochain valuesovereignty cooperativism greatplains funding platformcoop pico transcrowdfund usebitcoin shitcoin gigeconomy consommation workercoops economics cooperationjackson cooperation radical value business platformcooperatives exoplanets shopping displacement economic poplar shop companyculture plaintextaccounting MarketForLemons sovereignty crowdfund oops fairtrade RIPpla bankingCartel rope Datenbank Bitcoin startups radicalcooperation HenryGeorge scar plausible economíasolidaria disablitycrowdfund crowdfunding limitstogrowth ponzi companies theygrowupfast hermannplatz sharingiscaring techcoops plastikfrei plantprotein meetcoop disability micropatronage boarsplaining merz lgbtcrowdfund mehrplatzfürsrad monetize sineadoconnor cooperativas ua cryptocurrencies degrowth a2pico smallbusiness deliveroo intellectualproperty pla kommerzialisierung GitPay Fedigrowth gdp coopsday deplatforming timebank coop cooperativismo smallbusinesses europeancentralbank banknotes whyBitcoin cryptocurrency infoshop sine grow telecoop growth limits fuckfoodbanks btc values banks planetary plannedObsolence planet worldbank + Europe workercoop InformationFriction cooperatives accounting bank cooplife bitcoin noplanetb theWorkshop feministeconomics WealthConcentration valueflows coops holochain valuesovereignty cooperativism greatplains platformcoop pico coopstack transcrowdfund usebitcoin shitcoin gigeconomy consommation workercoops economics cooperationjackson cooperation radical value business platformcooperatives exoplanets shopping displacement economic poplar shop companyculture plaintextaccounting MarketForLemons sovereignty crowdfund ethereum oops fairtrade RIPpla bankingCartel rope Datenbank Bitcoin startups radicalcooperation HenryGeorge scar plausible economíasolidaria disablitycrowdfund crowdfunding limitstogrowth ponzi companies theygrowupfast hermannplatz sharingiscaring techcoops plastikfrei woocommerce plantprotein meetcoop disability micropatronage boarsplaining merz lgbtcrowdfund mehrplatzfürsrad monetize sineadoconnor postscarcity cooperativas ua cryptocurrencies coopjobs degrowth a2pico smallbusiness deliveroo intellectualproperty pla kommerzialisierung GitPay Fedigrowth gdp coopsday deplatforming timebank coop cooperativismo smallbusinesses europeancentralbank banknotes whyBitcoin cryptocurrency infoshop sine grow telecoop growth DesignForDisassembly limits fuckfoodbanks btc values banks planetary plannedObsolence planet worldbank - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT climate - YouStormOutlook heatwave energyconsumption energy energyuse SoilCarbon vampire renewables fuel clouds apollo racisme antira greenhousegas ClimateEmergency openscience renewableenergy ClimateMeme amp Nyurbinsky climateemergency climatechos gordoncampbell extremeweather ClimateAction climate climateracism renewable windenergy ClimateDenial ClimateProtection sciences ClimateStrike CycloneTauktae emissions coal climatecase climatestrike globalsouth ClimatePodcast weatherforecast kaspersky crisis foodcrisis vampiro energyvisions klimaatcrisis environment skypack climatecrises fossilfuel history_of_science earthscience tramp globalwarming mitigation limitededition weather ragingqueerenergy fossilcriminals camps climatecamp ClimateRefigees Podcast windpower sealevelrise ClimateCase globally globalization climatechoas endfossilfuels emergency CarbonOffsets heatwaves basecamp exitpoll Tyskysour pollution global parisclimateagreement science fossil energyefficiency OABarcamp21 mitmachen fossilfuels Climate sky climatescience energytransition climateaction ClimateCrisis storms warm biofuel globalviews headlamp whisky climatemitigation environmentalism Ruttecrisis climatecrisis + YouStormOutlook metoffice heatwave energyconsumption consumption riscos energy energyuse SoilCarbon vampire renewables fuel clouds apollo racisme antira greenhousegas ClimateEmergency openscience overconsumption renewableenergy climatejustice ipcc ClimateMeme amp Nyurbinsky climateemergency climatechos gordoncampbell extremeweather ClimateAction climate climateracism renewable windenergy ClimateDenial ClimateProtection sciences ClimateStrike CycloneTauktae emissions coal climatecase climatestrike globalsouth ClimatePodcast weatherforecast kaspersky crisis foodcrisis vampiro voteclimate energyvisions klimaatcrisis environment skypack climatecrises sustainability risc ar6 fossilfuel history_of_science earthscience tramp globalwarming mitigation limitededition weather ragingqueerenergy fossilcriminals camps climatecamp ClimateRefigees Podcast windpower sealevelrise ClimateCase globally globalization climatechoas endfossilfuels emergency CarbonOffsets heatwaves basecamp exitpoll Tyskysour pollution global parisclimateagreement science fossil energyefficiency OABarcamp21 climatecatastrophe mitmachen fossilfuels Climate sky climatescience energytransition climateaction ClimateCrisis storms RacistClimate warm biofuel globalviews headlamp whisky climatemitigation environmentalism Ruttecrisis climatecrisis - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT art - proudhon productivity cherrytree Fediverse oilpaint economiasolidaria arttips mastoartist paperart activism cali TraditionalArt Linke subresourceintegrity glitchart Art ocart robincolors resource urban article penandink webcomics startpage CommissionsOpen glassart afrique martialarts watercolours artsurbains artalley artvsartist2020 circulareconomy abstract artreference commission horrorart Earthquakes poe nomadbsd proxmoxve MartyMacMarty tgif coloringpage dccomics colored inkscape blink artificalintelligence draw circuitsculpture ttip watercolor proceduralart existentialcomics resources poetesss memes pinksterlanddagen ghibligif speedpaint SankeyCharts bengarrison subpoena autisticartist barrigòtic art sona animalart krita foraBolsonaroGenocida insights FreeColouringPage anthroart urbanart sigh queerart deviantart communityresources desigualdad pastel fantasyart drawings 20thcenturyillustration grafana daria artdeco adultcolouring source J19forabolsonaro collective openstreeetmap cryptoart politicalprisoners fantasy collage jordanlynngribbleart ToryParty educpop TheArtsHole linksunten risograph pro links CodeZwart thinkabout dndmemes fanfic articles protein forabolsonaro PartyPooperPost harmreductionart adhdmeme MastoArtHeader openra demoscene witch FreeArtLicense wallpaper generative political agriculture streetart coverart streetcomplete fountainpen stickers partners watercolour economy combyne freeculture fiberart PalestinianPoliticalPrisoners jet labyrinth educators mermay dpa artsale edu MastoArt particl PrisonNews FediverseApp urbansketchers ParticlV3 creativetoots culture ganart evenepoel opencl fiberarts polArt ink painting Leitartikel marten opencoop digitalart comic flyingonthewater kenmurestreet libreculture sartre artwork mandala b3d politicalcartoon blackart artsderue makingcomics glitch politicalprisoner junkart wallpapers railway linker riso xkcd supportartists proctorio drawtober startinblox comics intelligence linkinbio conceptart mastoart urbanterror illustration artopencall Hinkley gnuimagemanipulationprogram os studioghibli 2MinuteSketch wireart cartoon artistontwittter oc csa AccidentalGraffiti eink OriginalCharacter farts hattip poezio webcomic fleischproduktion DigitalArt pinkwashing partnership potentieldememe oilpainting kickstarter furryart twinkle DisabledArtist unixstickers pink fursona afriquedusud comicsans inkjet generativeart VaccineApartheid sticker enbyart originalart arts heartmind artbreeder 17maart fart TsunderdogArt videoart ivalice adultcoloring djmartinpowers arttherapy Cartudy extreemrechts fractal enby TattoosOfTheFediverse doodle artikel WorldLocalizationDay colouringpage worldwaterday NFTart netart signalstickers artschool digitalpainting intel politicaltheatre artvsartist dorktower maart abstractart drawing sig circular adhd sculpture artist pcbart meme cultureshipnames concretepoetry artwithopensource pinkwug Streeck VTMtober commissions pronouns opencallforartists DesolateEarthForThePoor VizierOpLinks commissionsopen fanon KartaView alroeart article17 fountainpenink MartinVanBeynen peppertop speedpainting animalpainting visionaryart blackartist worldpay figureart zine artists heart quickdraw error supportthearts genart urbanfantasy stickerei CurzioMalaparte tree lineart smartcard pixelart alisajart openframeworks professor networknomicon openrailwaymap politicalpolicing Earthstar JuliaHartleyBrewer fan digitalArt artistsOfMastodon glitchsoc paintings mermay2021 + proudhon productivity cherrytree Fediverse oilpaint economiasolidaria arttips mastoartist paperart libreart cali TraditionalArt Linke subresourceintegrity glitchart Art ocart robincolors resource urban article penandink webcomics startpage CommissionsOpen glassart afrique martialarts watercolours artsurbains artalley artvsartist2020 circulareconomy abstract artreference commission horrorart Earthquakes poe nomadbsd proxmoxve MartyMacMarty tgif coloringpage dccomics colored inkscape blink modelrailway artificalintelligence draw circuitsculpture ttip watercolor proceduralart existentialcomics FediverseGuide resources poetesss memes pinksterlanddagen FediverseForum ghibligif speedpaint SankeyCharts bengarrison subpoena autisticartist barrigòtic art sona animalart krita foraBolsonaroGenocida insights FreeColouringPage anthroart urbanart sigh queerart deviantart communityresources desigualdad pastel fantasyart drawings 20thcenturyillustration grafana daria artdeco adultcolouring source J19forabolsonaro collective openstreeetmap cryptoart politicalprisoners fantasy collage jordanlynngribbleart theGalley ToryParty educpop TheArtsHole linksunten risograph pro links CodeZwart thinkabout dndmemes fanfic articles protein forabolsonaro PartyPooperPost harmreductionart adhdmeme MastoArtHeader openra demoscene witch FreeArtLicense wallpaper generative political streetart coverart streetcomplete fountainpen stickers partners watercolour economy combyne freeculture fiberart PalestinianPoliticalPrisoners jet labyrinth educators mermay dpa artsale edu MastoArt particl PrisonNews FediverseApp urbansketchers ParticlV3 creativetoots culture ganart evenepoel opencl fiberarts polArt ink painting Leitartikel marten opencoop digitalart comic flyingonthewater kenmurestreet libreculture sartre artwork mandala FediverseTown b3d politicalcartoon blackart artsderue makingcomics glitch politicalprisoner junkart wallpapers railway linker riso xkcd supportartists proctorio drawtober startinblox comics intelligence linkinbio conceptart mastoart urbanterror illustration artopencall Hinkley gnuimagemanipulationprogram os studioghibli 2MinuteSketch wireart cartoon artistontwittter oc csa AccidentalGraffiti eink OriginalCharacter educator farts hattip poezio webcomic fleischproduktion nekodoodle DigitalArt pinkwashing partnership potentieldememe oilpainting kickstarter furryart twinkle DisabledArtist unixstickers pink fursona afriquedusud comicsans openstreetmap inkjet generativeart VaccineApartheid sticker enbyart originalart arts heartmind artbreeder 17maart fart TsunderdogArt videoart ivalice adultcoloring djmartinpowers arttherapy Cartudy extreemrechts fractal enby TattoosOfTheFediverse doodle artikel WorldLocalizationDay colouringpage worldwaterday NFTart netart signalstickers memex artschool digitalpainting intel politicaltheatre artvsartist dorktower maart abstractart drawing sig circular adhd sculpture artist pcbart meme cultureshipnames concretepoetry artwithopensource pinkwug Streeck VTMtober commissions pronouns opencallforartists DesolateEarthForThePoor VizierOpLinks commissionsopen fanon KartaView alroeart article17 fountainpenink MartinVanBeynen peppertop speedpainting animalpainting visionaryart blackartist worldpay figureart zine artists heart quickdraw error supportthearts genart urbanfantasy stickerei CurzioMalaparte tree lineart smartcard pixelart alisajart openframeworks professor smolzine networknomicon openrailwaymap politicalpolicing Earthstar JuliaHartleyBrewer fan digitalArt artistsOfMastodon glitchsoc paintings railways mermay2021 - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT sport - billiard darts olympics2020 swim motorsport snooker locksport swimming trailrunning marathon hockey bouldering diving baseball Millwall mma mammal sailing athletics nook dumpsterdiving sportsball skating skiing sport footballers climbing football combatsports golf + billiard darts olympics2020 swim olympics motorsport snooker sports locksport swimming trailrunning marathon hockey aikido bouldering diving baseball Millwall mma mammal sailing athletics nook olympic dumpsterdiving sportsball bing skating skiing sport footballers climbing football combatsports golf - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT + + + games + miniature appdesign gameofshrooms minecraft soloRPG nbsdgames karma tetris99 gamestop libregaming ageofempires mondragon BiophilicDesign videogame ksp TerraNil productdesign dungeonmaster gogodotjam AudioGame runequest miniatures dragonfall boardgames computergames creature fucknintendo fudgedice angrydesigner gameassets gamestonk fossgaming videogames FediDesign gameboy puzzle indiegames gamedesign shadowrun spot godotengine adventuregames chess gamejam nintendoswitch mudrunner mud indiegame game 0ad dragon playlog gameart orca sdg lovewood designfail opengameart sign asset gilgamesh fudgerpg ttrpg fudge gamedev freegames guildwars2 creaturedesign bideogames adventuregame TetrisGore gaming gamemaker gameing nintendo roleplayinggames itch unvanquished gamesdonequick Gamesphere devilutionx rpg gamespot tetris dosgaming supertuxkart freegaming DnD socialdesign karmaisabitch cyber2077 godot gamestudies tarot cyberpunk2077 gamesforcats FreeNukum spelunkspoil boardgaming supermariomaker2 neopets minetest omake guildwars dice dnd games playing + + Tue, 10 Aug 2021 08:34:29 UT bots posthumanism mrrobot human dehumanification Militanzverbot nobot botanists humanity militanzverbot Sabot44 humanrobotinteraction therobots humanetechnow verbote humankind - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT events - neverforget TuesdayVibe award daffodilday carbonemissions OONIbday waybackwednesday thursdayvibes fridayfilm todayilearned ShowYourStripesDay thursdaythought sun futuroparanissan IndigenousPeoplesDay5 notifications hissyfit ddosecrets solo throwbackthursday nissan valentinesday adventskalender live dos Day deepthoughts solorpg thingaday idahobit screenshotsaturday warmingup thursdaythoughts fridays ipv hackathons thursdaymorning Gesundheitskrise throwback RomaDay assweek animalsweatersunday justwatched TooMuchScreenTime beethoven250thbirthday valentine humanrightsday time followfriday wednesdaythought afediversechristmas whydopeopledoshitlikethis birthdaypresent festivals wednesdaymotivation early MayDay2021 SwissOvershootDay IllustrationDay bigbrotherawards cccamp19 lovewhereyoulive screenshot thelibrary PostLikeYouThinkACrabWouldSunday showerthoughts BIJ1 worldpenguinday animal ScreenshotSaturday beethoven anarchymonday treibhausgasemissionen solokey tipoftheday Verkiezingsfestival primeday paperoftheday bundesnetzagentur thimblefulthursday FreeAssangeYesterday 100DaysToOffload hackathon ff kids holiday folklorethursday LURKbirthday tomorrowspaperstoday wenbiesday punday ipv4flagday ipv6 christmas livecoding verfassungsschutz weeknotes LINMOBlive week FlashFictionFriday mothersday gsd koningsdag scree concert folklore festival FridayFolklore pride poll screenshottuesday animals VerkiezingsfestivalBIJ1 towertuesday fujifilmxt2 Nakbaday kdenlive dontstarve onthisday GlobalMayDay2021 simplescreenrecorder insideoutsockday screenshots livestream blissos whiskerswednesday BowieDay morningcrew theskytoday InternationalAsexualityDay tzag TinyTuesday FridaysForFuture sunday notification Koning weekendvibes screenshotsunday worldenvironmentday2021 showerthought library koningshuis cree VerseThursday liverpool waitangiday esc2021 bigbrotheraward caturday adayinthelife goodmorning Caturday day InternationalCheetahDay flatfuckfriday songfestival ItchCreatorDay iss RabbitRoadTrip2021 interestingtimes sideprojectsunday birthday sixonsaturday supdate StPatricksDay2021 koningsdag2021 wordoftheday theweeknd christmaslights AfricaDay livefree CancelCanadaDay worldenvironmentday fridaysforfuture nationallibraryweek meetup FathersDay transpride sex kidsthesedays rechtsextreme + neverforget TuesdayVibe award daffodilday carbonemissions OONIbday waybackwednesday thursdayvibes fridayfilm todayilearned ShowYourStripesDay thursdaythought sun futuroparanissan IndigenousPeoplesDay5 notifications hissyfit ddosecrets solo throwbackthursday nissan valentinesday adventskalender live dos livehack Day deepthoughts solorpg thingaday idahobit screenshotsaturday warmingup thursdaythoughts fridays ipv hackathons thursdaymorning Gesundheitskrise throwback RomaDay assweek animalsweatersunday justwatched TooMuchScreenTime beethoven250thbirthday valentine humanrightsday time followfriday wednesdaythought afediversechristmas whydopeopledoshitlikethis birthdaypresent festivals wednesdaymotivation early MayDay2021 SwissOvershootDay IllustrationDay bigbrotherawards cccamp19 lovewhereyoulive screenshot thelibrary PostLikeYouThinkACrabWouldSunday showerthoughts BIJ1 worldpenguinday animal ScreenshotSaturday beethoven anarchymonday treibhausgasemissionen solokey tipoftheday Verkiezingsfestival future primeday IRL paperoftheday bundesnetzagentur thimblefulthursday FreeAssangeYesterday 100DaysToOffload iScreech hackathon ff kids holiday folklorethursday LURKbirthday tomorrowspaperstoday wenbiesday punday ipv4flagday ipv6 christmas livecoding verfassungsschutz weeknotes LINMOBlive week FlashFictionFriday mothersday gsd koningsdag scree concert folklore festival FridayFolklore pride poll screenshottuesday animals VerkiezingsfestivalBIJ1 motivation towertuesday doesliverpool fujifilmxt2 Docuthon Nakbaday kdenlive dontstarve onthisday GlobalMayDay2021 simplescreenrecorder insideoutsockday screenshots livestream blissos whiskerswednesday BowieDay morningcrew theskytoday InternationalAsexualityDay tzag TinyTuesday FridaysForFuture sunday notification Koning weekendvibes screenshotsunday worldenvironmentday2021 showerthought library koningshuis cree VerseThursday liverpool waitangiday esc2021 bigbrotheraward caturday adayinthelife goodmorning Caturday day InternationalCheetahDay flatfuckfriday songfestival ItchCreatorDay iss RabbitRoadTrip2021 interestingtimes sideprojectsunday birthday sixonsaturday supdate StPatricksDay2021 koningsdag2021 wordoftheday theweeknd christmaslights AfricaDay livefree CancelCanadaDay worldenvironmentday fridaysforfuture nationallibraryweek meetup FathersDay transpride sex kidsthesedays rechtsextreme - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT politics - hate biometrics conspiracytheory TakeOurPowerBack redessociais solidarität trump Anarchy association cia socialjustice neoliberalisme eee workerowned alwaysantifascist sabotage qtibpoc VivotecniaCrueldadAnimal solidarityeconomy pressfreedom community systemicracism wageslavery immigration antifascismo liberal telemetry dissent liberation unions endprisonslavery laws fascism farmersrprotest techtuesday warc skyofmywindow techthursday nooneisillegal capitale freedomofspeech anarchist prochoice freeexpression EthnicCleansing anticapitalist RacialHealing fascisme liberalisme humanrights Anarchisme crime leftists turkish Socialism ukpol FreeKeithLamar Antifascisme copwatch capitalismkills fireworks homeless menschenrecht left petition BorisJohnson meteorología independant antifaschismus freedom EURvalues greens techtalk bikesforrefugees housingcrisis techdirt ontologicalanarchy labourabolition techsit union tories abolitionnow anarchism wegmetdemonarchie abuse DefundThePolice nazis earthship SocialCritique repression legaltech technews pelmets Jurastil meto devimage legal meeting polizeigewalt dannenröderwald smalltech FediAntifa police nzpolitics multicast antifascists oilwars multiverse antropocene kommunismus censored postttruth technik rightorepair control nuclear bjp ThirdRunway conservatives multi seaslug UnitedInDiversity maidsafe testing nazisme hierarchy avatars chehalisrivermutualaidnetwork vat ImmigrationEnforcement election republicans opinie diversity solidarity techwear communitycontrol metantispecismo hypocrits slavery sociaalDarwinisme metoo Avanti anticiv refugeeswelcome Coronariots seashepherd ecotech mybodymychoice generalstrike fuckBiden call2power DefendDemocracy wildfire neoliberal antipolitics charity AntiLiberalisme abolition digitalfreedom transrightsarehumanrights ScottishElections2021 mayday unionyes again hatespeech fascists antropoceno policerepression LateStageOfCapitalism earth stopchasseacourre solawi ciencia smashturkishfascism afropessimism antivax fedibikes Electricians burntheprisons seamonkey qt trumpism cyberlaw bossnapping peerproduction policiaasesina atlantik dansenmetjanssen corporations iww pushbacksareillegal indianpirates DisabilityPolicy vice SomethingIsGoingWrongHere til labor intersectional commons choice depressionen feelthefreedom Riot corporatewatch postcapitalism intersectionalfeminism smalltechnology wageslave uspol frontex communism mutualaidpdx RemoveThePolice makecapitalismhistory deathvalley chipocalypse criminalization abolishpolice nationalisme oist methaan anarchisten Immigration competition biometric mh brexitreality neoliberalism NeverTrustSimone socialecology wald whistleblower wroclawskierewolucjonistki icons MutualAid MutualAidRequest capitalism technology ACAB prisons feministhackmeetings wealth supremecourt conspiracytheories corporatecrime DirectAction ChildLabour parliament communist daretocare KeirStarmer NoMoreEmptyPromises greenpeace digitalslavery bushfire censor decrecimiento helmet refugeesgr taoteching technopolice anarchismus policeviolence politiikka kapitalisme retrotechnology ZwartFront bipoc housing decriminalization decolonisation politics WarCommentary inclusivity parametric gravimetry bosch Megaprisons decreased publicknowledge antiracism government neocities greendatacenter SocialDarwinism repressions brightgreen poc privatisierung anarchisme wayfire feminist colonialism DominicCummings nzpol peoplepower homelessness Bookchin informationtechnology ClemencyNow Inauguration2021 arran Revolutionary techthoughts brexit anarchistaction antimonopoly tw privileged totalitarianism localelections raid privatisation stillwithher TyskySour Labour democraciasindical nonprofitindustrialcomplex death LabourLeaks riots freethemall bolsonarogenocida green SocialJustice neoliberaal corporateStateTotalitarianism labour BAME decolonizeyourmind alternative privilege antikapitalisme masssurveillance firejail hamas legalcounsel AbolishPrisonsAbolishPolice despotism mntreform damangecontrol earthovershootday palantir DecentraliseThePlanet anti surfaceworldblows ecofascism opentechnologyfund depression nuclearpower popularitycontest usestandardpowerjacksforfucksake pdxmutualaid PoliceTenSeven LhubTV SocietalChange facialrecognition ModiFailsIndia cotech politicaeattualità corruption florespondece hypocrisy BernieSandersMeme staterepression anarchy fire colonization Feminism propaganda dcc greenit endsars celebratingfreedom userfreedom Antillia corporateState SocialCentres decolonization pc digitalrights feminism freepress Lhub HightechProblems datacenter osstotd farm problem hochschwarzwald collaboration pentesting polizei neo democracy anarchistki Govts BelarusProtests powerpolitics bikes 18Source hungerstrike censorshipBook radicaltech 56aInfoshop saytheirnames witchesagainstwhitesupremacy gulag digitalmarketsact yes socialist conspiracy anarchistbookclub redandanarchistskinheads peace housingproject hostileenvironment technically lawyer corporate osint radicaldemocracy endmodernslaveryatsea PritiPatel nationaalparkdebiesbosch stonewallwasariot oiseau surveillance latestagecapitalism bos racist economiafeminista cancelculture postcolonial callfortesting dec AmbLastillaAlCor Selfsuffciency nonazis MexicanRevolution elections ACABPoland greatgreenwall RussellMaroonShoatz LhubSocial OctoberRevolution logitech methods Flatseal repressionen commonspub warcrimes sea policing white governance waldstattasphalt prisoners earthday2021 warrants policebrutality techshit earthday antirepression capitalismo borisjohnson wildfires ACABSpring2021 technopopulism Anarchist deepspeech notacopshop body johnson rhetoric press routerfreedom Anarchism mutuality StillTwitterpated whitehouse metropolitanpolice espresso LabourParty haltandcatchfire freedomofexpression censorship deathbycapitalism communities CancelCulture decolonize deconstruct HanauWarKeinEinzelfall musictechnology druglawreform keinmenschistillegal immigrationraids emmet racism fascisten decenterwhiteness Biden FossilFreePolitics ChineseAppBan multiplesklerose cooperative trespass modi antifa alternativen law prison chip LabourMovement deathtoamerica manipulation ParticipatoryCultureFoundation firetotheprisons consumer solidaritaet PlanetarySocial britpol financial gravimetrie BiodiversityDay Capitalism surveillancecapitalism leftist greenland general Revolution ukpolitics greenparty mdcommunity glenngreenwald support JeremyCorbyn blacklivesmatter freedomofthepress academicfreedom HeinsbergStudie apartheid FreeAlabamaMovement Anarchismus bundespolizei strike mononeon rentstrike evergreen equality otd dsa informationstechnik piracy liberty lawandorder feminismus migration power oiseaux techmess neoist edtech capitalismenumérique mutualaid capital waldspaziergang cymru multipleexposure socialsolidarityeconomy humanetechnology AbolishPrison solidaritynotcharity anarchists fascist righttochoice InformationAsymmetry socialcoop inequality vim apocalypseworld DefundSurveillanceCapitalism feministserver prisonersupport platformcapitalism decolonizeconservation anarchistprisoners whistleblowers polizeiproblem notallmen hf prisonabolition fightthepower UniversalBasicServices fuckcapitalism speech uselection IDPol Antifa deathtofascism mediafreedom lesanarchistes libertarianism Slavetrade met democracia antitrespass drugtesting populism selfcensorship consumerism greenwashing ourstreets reform MeToo extremist bright freespeech anticonsumerism kapital neorodiversiteit refugees BlackProtestLegalSupport riot BernieSanders acab ecology yesminister realcompetition antifascist SurveillanceCapitalism vimeo antifascism GlobalCapitalism Politics homeoffice bodyshaming empowerment whitepaper pdx seascape freewestpapua eris hambacherwald dui nyt justice powstaniewgetciewarszawskim sunnytech FolksWhoFailAtCapitalism expression feudalism espressif violence legalmatters academic tech capitalismodisorveglianza + hate biometrics conspiracytheory TakeOurPowerBack redessociais solidarität trump Anarchy association cia socialjustice neoliberalisme eee workerowned alwaysantifascist sabotage qtibpoc VivotecniaCrueldadAnimal solidarityeconomy pressfreedom community systemicracism wageslavery immigration antifascismo liberal telemetry dissent liberation unions endprisonslavery laws fascism farmersrprotest techtuesday warc skyofmywindow techthursday nooneisillegal capitale freedomofspeech anarchist prochoice freeexpression EthnicCleansing anticapitalist RacialHealing fascisme liberalisme humanrights Anarchisme crime leftists turkish Socialism ukpol FreeKeithLamar Antifascisme copwatch capitalismkills fireworks homeless menschenrecht left petition BorisJohnson meteorología independant antifaschismus freedom EURvalues greens photomanipulation techtalk bikesforrefugees housingcrisis techdirt ontologicalanarchy labourabolition techsit union tories abolitionnow anarchism wegmetdemonarchie abuse DefundThePolice nazis earthship SocialCritique repression legaltech technews pelmets Jurastil meto devimage legal meeting polizeigewalt dannenröderwald venturecapital FediAntifa police nzpolitics multicast antifascists oilwars multiverse antropocene kommunismus censored postttruth technik rightorepair control nuclear bjp ThirdRunway conservatives multi seaslug UnitedInDiversity maidsafe testing nazisme hierarchy avatars chehalisrivermutualaidnetwork vat ImmigrationEnforcement election republicans opinie diversity solidarity chipstrike techwear communitycontrol metantispecismo hypocrits slavery sociaalDarwinisme metoo Avanti anticiv refugeeswelcome Coronariots seashepherd ecotech reform2 mybodymychoice generalstrike fuckBiden call2power DefendDemocracy personhood wildfire neoliberal antipolitics charity AntiLiberalisme abolition digitalfreedom transrightsarehumanrights ScottishElections2021 mayday unionyes again hatespeech fascists antropoceno policerepression LateStageOfCapitalism earth stopchasseacourre solawi ciencia smashturkishfascism afropessimism antivax cognition fedibikes Electricians apartheidisrael burntheprisons conservation seamonkey qt trumpism cyberlaw bossnapping peerproduction policiaasesina atlantik corporations iww pushbacksareillegal indianpirates DisabilityPolicy vice SomethingIsGoingWrongHere til labor intersectional commons choice depressionen feelthefreedom Riot corporatewatch postcapitalism intersectionalfeminism smalltechnology wageslave uspol frontex quarantine communism mutualaidpdx RemoveThePolice makecapitalismhistory deathvalley NewPoliticalMap chipocalypse criminalization abolishpolice nationalisme oist methaan anarchisten Immigration competition biometric brexitreality neoliberalism NeverTrustSimone socialecology wald whistleblower wroclawskierewolucjonistki icons MutualAid capitalism technology ACAB prisons unsolicitedadvice feministhackmeetings wealth supremecourt conspiracytheories corporatecrime DirectAction ChildLabour FossilFreeRevolution parliament communist daretocare KeirStarmer NoMoreEmptyPromises greenpeace digitalslavery bushfire censor decrecimiento helmet refugeesgr taoteching technopolice anarchismus policeviolence politiikka kapitalisme retrotechnology ZwartFront bipoc housing decriminalization decolonisation politics WarCommentary inclusivity parametric gravimetry bosch Megaprisons decreased publicknowledge antiracism government neocities greendatacenter SocialDarwinism repressions brightgreen poc privatisierung anarchisme wayfire feminist colonialism DominicCummings nzpol peoplepower homelessness Bookchin informationtechnology ClemencyNow Inauguration2021 arran Revolutionary techthoughts brexit anarchistaction antimonopoly privileged totalitarianism localelections raid privatisation stillwithher TyskySour Labour democraciasindical nonprofitindustrialcomplex death fires LabourLeaks riots freethemall bolsonarogenocida green SocialJustice neoliberaal corporateStateTotalitarianism labour BAME decolonizeyourmind alternative privilege antikapitalisme masssurveillance hamas legalcounsel AbolishPrisonsAbolishPolice despotism mntreform damangecontrol earthovershootday palantir DecentraliseThePlanet anti surfaceworldblows ecofascism opentechnologyfund depression nuclearpower popularitycontest usestandardpowerjacksforfucksake pdxmutualaid PoliceTenSeven LhubTV SocietalChange facialrecognition ModiFailsIndia cotech antisemitism politicaeattualità corruption florespondece hypocrisy BernieSandersMeme staterepression anarchy fire colonization Feminism propaganda dcc greenit endsars celebratingfreedom Antillia corporateState SocialCentres decolonization digitalrights feminism freepress Lhub HightechProblems datacenter osstotd academictwitter farm problem hochschwarzwald collaboration pentesting polizei neo democracy anarchistki Govts antikapitalismus powerpolitics bikes 18Source hungerstrike censorshipBook radicaltech 56aInfoshop saytheirnames witchesagainstwhitesupremacy gulag digitalmarketsact yes socialist conspiracy anarchistbookclub redandanarchistskinheads peace housingproject hostileenvironment technically lawyer corporate osint radicaldemocracy endmodernslaveryatsea PritiPatel nationaalparkdebiesbosch stonewallwasariot oiseau surveillance latestagecapitalism bos racist economiafeminista cancelculture postcolonial Syndicalism callfortesting dec AmbLastillaAlCor Selfsuffciency nonazis MexicanRevolution elections ACABPoland greatgreenwall RussellMaroonShoatz LhubSocial OctoberRevolution bigproblems logitech methods Flatseal repressionen commonspub warcrimes sea policing white governance waldstattasphalt prisoners earthday2021 warrants policebrutality techshit earthday antirepression capitalismo borisjohnson wildfires fritolaystrike ACABSpring2021 technopopulism Anarchist deepspeech notacopshop body johnson rhetoric press routerfreedom Anarchism mutuality StillTwitterpated whitehouse metropolitanpolice espresso LabourParty haltandcatchfire freedomofexpression censorship deathbycapitalism communities CancelCulture decolonize deconstruct HanauWarKeinEinzelfall musictechnology EatTheRich druglawreform keinmenschistillegal immigrationraids emmet racism fascisten decenterwhiteness Biden kapitalismus FossilFreePolitics ChineseAppBan multiplesklerose todoist cooperative trespass modi NtechLab antifa alternativen law prison chip LabourMovement deathtoamerica manipulation ParticipatoryCultureFoundation firetotheprisons consumer solidaritaet PlanetarySocial britpol financial gravimetrie BiodiversityDay Capitalism surveillancecapitalism leftist greenland general Revolution ukpolitics greenparty mdcommunity glenngreenwald support JeremyCorbyn blacklivesmatter freedomofthepress academicfreedom wled HeinsbergStudie apartheid FreeAlabamaMovement Anarchismus bundespolizei strike mononeon rentstrike evergreen equality dsa informationstechnik piracy liberty lawandorder feminismus migration power IndividualSovereignty oiseaux techmess neoist edtech capitalismenumérique mutualaid capital waldspaziergang cymru multipleexposure socialsolidarityeconomy humanetechnology criminal AbolishPrison solidaritynotcharity anarchists fascist righttochoice InformationAsymmetry inequality vim apocalypseworld DefundSurveillanceCapitalism feministserver prisonersupport platformcapitalism decolonizeconservation anarchistprisoners whistleblowers polizeiproblem notallmen opensoundcontrol hf prisonabolition fightthepower UniversalBasicServices fuckcapitalism speech uselection IDPol Antifa deathtofascism mediafreedom lesanarchistes libertarianism Slavetrade PostTrade met democracia antitrespass drugtesting populism selfcensorship consumerism greenwashing ourstreets reform MeToo failedstatesaxony extremist bright freespeech comune anticonsumerism kapital refugee neorodiversiteit whitesupremacy SueveillanceCapitalism refugees BlackProtestLegalSupport riot BernieSanders texttospeech acab ecology yesminister realcompetition antifascist SurveillanceCapitalism vimeo antifascism GlobalCapitalism Politics homeoffice bodyshaming empowerment whitepaper pdx seascape freewestpapua eris AnarchistUnionofIranandAfghanistan hambacherwald dui nyt justice powstaniewgetciewarszawskim sunnytech FolksWhoFailAtCapitalism expression feudalism espressif violence legalmatters academic tech capitalismodisorveglianza - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT places - communedeparis lapaz luanda asunción salisbury nouakchott conakry kyiv enviromentalism moscow winchester cardiff saipan gibraltar dublin KlimaGerechtigkeit stuff catalunya dannibleibt avarua lilo wolverhampton hargeisa delhi niamey chișinău freestuff font colombo dundee brasília phnompenh mbabane danni belgrade rotterdam stasaph belmopan pyongyang hannover strawinsky calls ulaanbaatar oranjestad kali Reykjavik Barliman gaborone seattle ndjamena lancaster chelmsford raw singapore tuberlin preston lincoln kingedwardpoint abidjan nuuk york asshole pretoria papeete DreamtimeVillage washingtonstate bradford malé rhetorical robberfly sunderland zagreb gitega abudhabi flyingfishcove castries revil georgetown suffolk wickr hagåtña podman videoFreex oric ella lichtenberg videofeedback borikua basseterre hamburg southeastasia fonts afrika kinshasa Schadensersatzforderung streetartparis suva klimaatverandering valparaíso athens roseau sheffield baku aberdeen charlotteamalie antananarivo domi pristina RadentscheidJena bordeaux diff MakoYass videocalls santiago fsb sukhumi berlin urk bristol uptronicsberlin funafuti libreville newry radentscheid puertorico ClimateChange hanoi philipsburg tehran banjul prague Stockente rawhide andorralavella daw yerevan portauprince videoprojects sensorica mewcastle dakar asu paramaribo tifariti durham CrisiClimatica capetown rigaer94 dma tirana klima ankara ipswich managua lisbon bishkek amsterdam climatchoas kent klimaat EastVirginia portonovo santodomingo wakefield bangkok texas coventry bucharest kathmandu aden madrid paris14 sanjuan vienna kingston stuttgart Utrecht inverness kabul damascus stockholm douglas ClassOf2015 willemstad brighton klimaschutz klimaatnoodtoestand hibernoenglish thehague panamacity RassismusTötet beirut belfast amman newdelhi tórshavn nottingham nouméa oslo alofi gustavia paris cockburntown berlinale manchester dominio ottawa classical buch stepanakert portofspain klimakrise class fsberlin honiara berniememe asmara florida nicosia helsinki taipei salford tegucigalpa bridge tokyo tashkent larochelle vr gabocom MadeInEU sarajevo algiers southampton KlimaKrise nairobi muscat monaco riyadh flying lusaka perth wellington wick bissau juba mariehamn majuro parisagreement mumiaabujamal norwich buenosaires douglasrushkoff ngerulmud dhaka berlinhateigenbedarf guatemalacity washington bedarf vatican kuwaitcity martlesham Erdmannhausen londonboaters SystemChangeNotClimateChange bern mexicocity amap bratislava myasstodontownhall bridgetown delhipolice stokeonTrent crowsnestpass leeds tunis manila warwickshire rigaer94verteidigen arctic stanley matautu copenhagen hereford barcelona lomé videocall budapest ouagadougou mogadishu PrawnOS freetown victoria bangor lora brazzaville portmoresby ashgabat kampala Klimaatalarm gigabitvoucher kirigami webassembly yorkshire elaaiún kalimantan vilnius ContourDrawing bloemfontein gnuassembly swansea sucre london passalong marseille berniesanders pagopago bradesestate oakland vaduz birmingham addis lisburn nürnberg naypyidaw derry CassetteNavigation khartoum baghdad bandar truro moroni cuirass rigaer lehavre klimaliste portvila kingstown armagh Klima ulm ChrisCrawford reykjavík lofi manama accra mewport windhoek fortworth nukualofa classic ciutatvella tbilisi canberra quito maputo cetinje adams putrajaya lichfield ramallah solimaske oslotown bogotá warming portsmouth dodoma berkeley harare stirling havana warsaw klimapolitik rigaer94bleibt münster valletta snes localberlin ljubljana bamako leicester kualalumpur peterborough podgorica rabat cotonou oranje plymouth seoul westminster neumünster Portland dushanbe bangui aotearoa canterbury westisland tskhinvali palikir caracas brussel jamestown rome gloucester munich cambridge ripon carlisle freestuffberlin wells chichester sãotomé jakarta daressalaam sansalvador seo apia essex klimawandel yaren cairo jerusalem brussels kigali southtarawa beijing minsk montevideo vientiane philips maseru klimaatopwarming hamilton lorawan lurk doha klimaatwake worcester tripoli celtic portlouis stalbans lima adamstown deventer weimar abuja fuckalabamapower saw lilongwe nassau lobamba heathrow nyc oxford fly montreal klimaatzaakshell rawtherapee dili feedback thesprawl riga r94 assembly lesbos monrovia nursultan Neuzulassung caste gab sanjosé klimaatrechtvaardigheid marigot islamabad fb malabo tallinn sahara thimphu klimaatzaak exeter oranjeklanten klimanotstand chester yaoundé praia bujumbura strawberries washingtondc derby sofia skopje + communedeparis lapaz luanda klimakatastrophe asunción salisbury nouakchott conakry kyiv enviromentalism gadgetbridge moscow winchester cardiff saipan gibraltar dublin KlimaGerechtigkeit stuff catalunya dannibleibt avarua lilo wolverhampton hargeisa delhi niamey chișinău freestuff colombo dundee brasília StupidComparisons brushes phnompenh mbabane danni belgrade rotterdam stasaph belmopan detroit ghent pyongyang hannover strawinsky calls ulaanbaatar oranjestad kali Reykjavik Barliman gaborone seattle ndjamena lancaster chelmsford raw singapore classicalmusic tuberlin Lanarkshire feedbackwanted preston lincoln kingedwardpoint abidjan nuuk york asshole pretoria papeete DreamtimeVillage washingtonstate bradford malé rhetorical robberfly sunderland zagreb gitega abudhabi flyingfishcove castries revil georgetown suffolk wickr hagåtña podman lochlomond videoFreex oric ella lichtenberg videofeedback borikua basseterre hamburg southeastasia afrika kinshasa Schadensersatzforderung streetartparis suva klimaatverandering valparaíso athens roseau sheffield baku aberdeen charlotteamalie antananarivo domi pristina northumberland RadentscheidJena bordeaux diff MakoYass videocalls santiago fsb sukhumi berlin urk bristol uptronicsberlin funafuti libreville newry rush radentscheid puertorico ClimateChange hanoi philipsburg tehran banjul prague Stockente rawhide andorralavella daw yerevan portauprince videoprojects sensorica mewcastle dakar asu paramaribo tifariti durham CrisiClimatica capetown rigaer94 dma tirana klima ankara ipswich managua lisbon bishkek amsterdam climatchoas kent klimaat EastVirginia portonovo santodomingo wakefield bangkok texas coventry bucharest kathmandu aden buchtipp madrid cleanarchitecture paris14 sanjuan vienna kingston stuttgart Utrecht inverness kabul damascus stockholm douglas ClassOf2015 willemstad brighton klimaschutz klimaatnoodtoestand hibernoenglish thehague panamacity RassismusTötet beirut belfast amman newdelhi tórshavn nottingham nouméa oslo alofi gustavia paris fromembers cockburntown berlinale manchester dominio ottawa classical buch stepanakert portofspain klimakrise class fsberlin honiara berniememe asmara florida nicosia helsinki anywhere taipei salford tegucigalpa bridge tokyo tashkent larochelle vr gabocom MadeInEU sarajevo algiers southampton KlimaKrise nairobi muscat monaco riyadh flying lusaka perth wellington wick bissau juba mariehamn majuro parisagreement mumiaabujamal norwich buenosaires douglasrushkoff ngerulmud dhaka berlinhateigenbedarf guatemalacity washington bedarf vatican kuwaitcity martlesham Erdmannhausen Puntarella londonboaters SystemChangeNotClimateChange bern mexicocity amap bratislava myasstodontownhall bridgetown delhipolice stokeonTrent crowsnestpass leeds tunis manila warwickshire architecture rigaer94verteidigen arctic stanley matautu copenhagen hereford barcelona lomé videocall budapest ouagadougou mogadishu PrawnOS freetown victoria bangor lora brazzaville portmoresby ashgabat kampala Klimaatalarm gigabitvoucher kirigami webassembly yorkshire elaaiún kalimantan vilnius guineabissau ContourDrawing bloemfontein gnuassembly swansea classe sucre london passalong marseille berniesanders pagopago bradesestate oakland vaduz birmingham addis lisburn nürnberg naypyidaw derry CassetteNavigation khartoum baghdad bandar truro moroni cuirass rigaer lehavre klimaliste portvila kingstown armagh Klima ulm ChrisCrawford reykjavík lofi manama accra mewport windhoek fortworth nukualofa classic ciutatvella tbilisi canberra quito maputo cetinje adams putrajaya lichfield ramallah solimaske oslotown bogotá warming portsmouth dodoma berkeley harare stirling havana warsaw klimapolitik rigaer94bleibt münster valletta snes localberlin ljubljana bamako leicester kualalumpur peterborough podgorica rabat cotonou oranje plymouth seoul westminster neumünster Portland dushanbe bangui aotearoa theCellar canterbury westisland tskhinvali palikir caracas brussel jamestown rome gloucester munich cambridge ripon carlisle freestuffberlin wells chichester sãotomé jakarta floraspondence daressalaam sansalvador seo apia essex klimawandel yaren cairo jerusalem brussels kigali southtarawa beijing minsk montevideo vientiane philips maseru klimaatopwarming hamilton lorawan lurk doha klimaatwake worcester tripoli celtic portlouis stalbans lima adamstown deventer weimar abuja fuckalabamapower saw lilongwe nassau lobamba heathrow nyc oxford fly montreal klimaatzaakshell rawtherapee dili feedback thesprawl riga r94 assembly lesbos monrovia nursultan Neuzulassung caste gab sanjosé klimaatrechtvaardigheid marigot islamabad fb malabo tallinn sahara thimphu seattleprotestnetwork klimaatzaak exeter oranjeklanten klimanotstand chester brest yaoundé praia bujumbura strawberries washingtondc derby sofia skopje - Tue, 13 Jul 2021 08:43:43 UT - - - employment - justworked futureofwork InterviewQuestions jechercheunjob mywork remote employees hiring workingfromhome ProgrammingJob reproductivework frame workinprogress bullshitjobs car workplace DigitalNetwork antiwork workshops kreaturworks workers worklog sexworkers remotejob mainframe remotework remotejobs migrantworkers job culturalworkers DjangoJob teamwork framework hire KDEGear careers hirefedi career SocialNotworks workshop bedfordshire illustratorforhire tidyworkshops carework nowhiring KDE rds KDEGear21 obs workersrights obsolescence records KDEFrameworks plannedobsolescence work hertfordshire flossjobs jobs workflow precariousworkers carddav sexworker theworkshop nerdsnipe - - Tue, 13 Jul 2021 08:43:43 UT - - - gafam - zuckerberg caringissharing ads apple antitrust SpringerEnteignen peoplefarming deletewhatsapp advertisingandmarketing chromevox GoogleDown aws AppleSearch Floc bankruptBezos googlesearch googleio mycologists bringBunysBack youtube Goggle twitterkurds banadvertising chromebook fuckfacebook headset arcgis ffs AmazonMeansCops facebook wandering 100heads 20thcenturyadvertising amazon googlevoracle amazonprimeday dystopia microsoftgithub farcebook myco boycottinstagram FlocOff stopgafam genoegisgenoeg legislation amazonprime deletewhatsappday amazonring Gafam googleplus soldering GoogleForms HaringeyAnti delete FoodSharing lobbyregister degooglisation florespondance linkedin siri Facebook LeiharbeitAbschaffen advertising monopolies googleanalytics ausländerzentralregister adtech fuckgoogle storing plottertwitter failbook kadse microsoft deletechrome alanturing dtm poledance HeadscarfBan twitter skype azure chrome logistics googledoodles hildebrandt corporateGiant Tracking uitkeringen FlocOffGoogle sidewalk plot zuck nogafam youtubedl degoogled Google youtubers google stemverklaring gis walledgarden GAFCAM dt GooglevsOracle dotcoms deleteyoutube datafarms Instagram walledgardens agistri appleevent offseting Hypnagogist appleii facebookoversightboard fascistbook FuckGoogle degoogle boringdystopia fuschia ohneamazon appleiie deleteinstagram ungoogled ring stopgoogle affordances googledown decentring gafam inspiring oracle killedbygoogle fuckoffgoogle dance deletefacebook gradschool fakebook GoogleIsBad fuckoffgoogleandco office365 lordoftherings turingpi amazonas instagram TrackingFreeAds FlocBloc playstore synergistic bigtech boycottamazon whatsapp mytwitteranniversary deleteamazon bluesky Amazon - - Tue, 13 Jul 2021 08:43:43 UT - - - people - Melissa harold paul Zachary JusticiaParaVictoria danielle dylan scott Barbara Kenneth theresa Denise FrankLeech louisrossmann Jesse Adam justin JonathanCulbreath elinorostrom katherine judith Karen Patricia russell Metalang99 juan diane Rebecca donna LouisRossmann olivia peter troy William denise NathanDufour Betty evelyn Christina brittany Jennifer Gregory Wayne Andrychów ethan Ralph Peter ecc americalatina jacobites jean laura betty nathan brownmark margaret alexanderlukashenko Bryan Virginia Jose Rose eric james BomberBradbury david Joshua christine haaland Billy CapitolRiot ostrom natalie daniel Jonathan Michael susan George johnny bookmark MichaelWood Lauren christina Amy kevin Natalie kenneth noahkathryn Lawrence aaron donaldtrump gregory LindaLindas Amber alexa Robert Edward Patrick Rachel Verwaltunsgericht willemalexander bruce Forms dennis LegalCannabis Kayla frank Diane AliceHasters Donna Jack Paul Janice Brenda alexis sylvanasimons timothy vincent Alice sarah amy Daniel RobertKMerton jeff charlotte carolyn Emma Kyle Sean emily linda Olivia Eugene johnpilger Donald janet ryan Bookmarker stdavids RichardDWolff bryan DonnaStrickland Hannah anna doctorow MalcolmJohnson Catherine Alexander Christopher bob doris Anthony singlemarket Jean diana Beverly frances Sarah margaretthatcher Jordan peterrdevries JensStuhldreier Anna Ethan Amanda jeremy donald NatashaAKelly mark matthew julie ryanair BenSchrader DrJessicaHutchings stephanie Jerry SEKFrankfurt Diana David Linda adam richard henry RoyalFamily Isabella elizabeth nachrichten steven jessica Walter dry jeffrey Kevin Justin mountanMaryland grace martinluther PeterGelderloos brandon mary anwarshaikh jamesbaldwin sharon nicholas Benjamin GeorgeFloyd amanda Emily Ruth heather stephenlawrence albert julianassange Julie marktwirtschaft nancy stephen Cannabis James CarlSpender Megan bettydog Raymond eugenetica michelle frankgehtran Nancy Fedimarket Frances Henry andrew kevinRuddCoup Jessica zurich julia marketing Dorothy LoganGrendel Jason Charles JonathanMorris Danielle Brandon jose noamchomsky virginia beverly obituary ronald Bob madison alberta ceph Helen MarkoBogoievski Jeff helen Sophia larry bookmarks dorothy Dennis monbiot Nicholas Frank jack Stephen Janet ScottRosenberg Alexis Pamela Jacqueline Dylan roy brenda jackal jesse Roger Jeffrey Brittany Shirley putkevinback Nathan christopher Carol Susan jason Philip Logan sandra jacob rose isabella Cynthia Joan jackieweaver aldoushuxley Maria martha Randy SarahEverard carl kyle karen raymond alice jerry carol RussellBrown Victoria Steven Douglas Lisa JonathanZittrain Julia joshua jacqueline Ashley assange eugene Bruce Albert Austin thomas Evelyn Gary Scott kimberly lawrence virgin jennifer Russell austin erdogan betterlatethannever ShhDontTellJack logan Laura Chris walters Teresa GeorgeGalloway Aaron Keith brian marktwain maryanning LamySafari maria Joseph Andrew Vincent Katherine Joyce NathanJRobinson lauren Ryan amber davidgraeber alan ralph princephilip DennisTheMenace megan Kathleen sophia Cheryl abigail cynthia john richardstallman Alan AnnihilationOfCaste Debra GeorgeHoare arthurgloria mariadb LouisFurther Christine marilyn anthony chris Berichte Elizabeth sean Louis Larry AnnSophieBarwich christian deborah billy Abigail joesara AndreaBeste keith Jeremy CapitolRiots markkennedy zachary ruth Grace teresa Doris benjamin Willie george methane barbara scottish Charlotte philip DaveCunliffe ethanzuckerman randy Margaret Heather Bradley Jacob shirley pamela Matthew Nicole joan judy Kelly savannah Brian melissa Sandra stallman markstone joseph oberverwaltungsgericht andrea shamelessselfplug Joe Sara robert alicevision aaronswartz better Bobby emma willie william angela rich SachaChua samuel Postmarketos tyler Thomas John kroger patricia ashley bobby roses kelly fuckamerica ThomasCahill hannah Carolyn Ann CrimsonRosella Jeangu gary wayne Marilyn Deborah christenunie rms Sharon gare Mary frankfurt Samuel BreonnaTaylor Mark walter rebecca helendixon Madison Juan lisa cheryl janice jeffreyepstein Christian gerald Timothy roger edward bradley Gerald PiersMorgan patrickrachel framalang Kimberly steve Gabriel Marie EmmaFerris PeterHoffmann PaulBaran louis kathleen Arthur Gloria terry royals freejeremy bernardhickey Richard jonathan Harold shame Roy samantha DavidSeymour Carl chalice Eric AndreiKazimirov RebeccaHarvey relationships visuallyimpaired nicole Andrea Judith Terry Stephanie Johnny Angela Noah Kathryn RichardBoeth Ronald AskVanta Michelle Theresa gabrielmarie Samantha Judy michael charles GeorgeGerbner Tyler amaryllis DouglasPFry kayla catherinealexander Martha debra JohnMichaelGreer stevewozniak joyce - - Tue, 13 Jul 2021 08:43:43 UT - - - activitypub - followerpower FederatedSocialMedia mastodevs kazarma activitypub activertypub tootfic askthefedi fedivision pleroma losttoot Rss2Fedi PeerTube CreativeToots devices gofed getfedihired collaborate pixelfedlabs hometown homelab RedactionWeb fediblock fediverso lazyfedi happyfedi2u federation Invite2Fedi instances fedilab bandsofmastodon Wallabag blocks pixiv mastotips sammelabschiebung toot fedilabfeature mastodev fediversetv pixel Ktistec mastodontips catsofthefediverse mastotip pixel3a wallaby MastoDev friendica mastodontip talesfromthefediverse mastofficina fleamarket ap_c2s hiveway bands mastodonart mast Moneylab Mosstodon Adblocker fedionly DeveloperExperience askthefediverse misskey collaboraoffice activitypub_conf plsboost BlackFedi joinmastodon AskPixelfed siskin socialhub followers fediart blocking fedifreebies Metatext SocialMediaReimagined fediverse13 mondkapjesplicht Pixelfed contentwarnings pixelfed labournettv fediverseplaysjackbox mapeocolaborativo fedihive greeninstances fedidb block FediMemories mastectomy Feditip devs fablab fediverseparty collabathon Dev Fediseminar onlyfedi admin socialcg teamtoot fedbox FediMeta sponsorblock SocialNetworkingReimagined tusky retoot contentwarning peertubers imagedescription joinpeertube anastasia feditips tootcat dnsssecmastery2e fedizens Mastodon following epicyon afediversechat andstatus peertubeadmin leylableibt fediversefleamarket mastomagic YearOfTheFediverse mastodob fediadmin pleaseboost mastodonhost mond pixeldev pixelfont timeline socialmedia tips wedistribute fedivisionCollab fosstodon instanceblock softwaredevelopment freetoot mastodonmonday fedihelp fediWhen fedicat asta collaborative isolategab greenmastodon FediverseFixesThis fedireads pixeldroid networkTimeline PeertubeMastodonHost boost AskFediverse Bookwyrm federated socialhome greenfediverse WriteFreely microblocks collabora fedivers MastodonMondays fediverse imagedescriptions mastobikes gbadev lemmy Fedilab bunsenlabs mastoadmin smithereen hackerstown uadblock c2s FediverseFutures latenighttoots mastodon pcmasterrace developingcountries boostswelcome PixelfedDev fedi fediversefriday mondkapje fediplay activity widevine peertube fieldlabs mastomind lab BlackMastadon fedeproxy boosten tootorial boostwelcome lazyfediverse mastoaiuto mobilizon Fediverse13 lazy gemifedi activityPubRocks - - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT internet - datasette onlinesafetybill linkeddata markdown selfsufficiency webgl LoveWattsBLM decentralised immersiveweb pep decentraliseren i2p sceptic earlyinternet Clubhouse CooperativeClouds spam firefox redecentralize NYCMesh decentral socializing Burocratic toxicmasculinity staticsitegenerator wikipedia maps rtmp PlasticFreeJuly dataprotection decentralization inclusiónsocial decentralize w3c datacollection files dotConism offlineimap DutchPolitics internetaccess agnostic gotosocial geminispace archivists gaza selfhosted piratenpartij mapuche videohosting DarkPatternsFTC metafilter maille meta wikibase CooperativeTechnology torrent mailab geocaching freenode MollyBrown mailfence bot tox k9mail nylasmail data socialism basemap webarchive sitejs meshroom anticolonial VerkehrsswendeJetzt Jabbber worldbusterssocialclub publicserviceinternet networks criticism bioinformatics online openddata centralisation internetarchaeology WordPress darkages hiddenServices chainmail datarecovery self elinks saferinternetday selfhost text SeattleHellDay contentmoderation distributed OperationPanopticon mappe mydata webhosting decentralizedweb mailman SOASJusticeforCleaners natto p2pleft socialdistancing router protection rne dataretention speedtest ublockorigin bigdata routeros internetofthings greenhosting selfhosting forkawesome communityhosting TikTok tilde CriminalJusticeBill brave panopticon aldi icann selfsustaining hosting mailart DAOs discourse digitalcolonialism weblate libera PeerToPeer wikis dns decentralizetheweb stripe service openstandards nojs ejabberd freifunk oauth Anticon tic foxes hypercore CDNsAreEvil meshtastic protonmail TubEdu standards StuffCircuit yourdataisyourdata internetfreedom mirroring onlineWhiteboard gemini antarctic zeit webui InternetCrimeFamily wlan boilemMashEmStickEmInAStew internetBanking SmallWeb fedwiki redessociales fleenode ircd coopcloud cw internetshutdown democratic criticalmass datadetox clearnet cdn cloudflared liberapay pinterest brahmaputra distributedcoop xmpp semanticweb identicurse socialnetwork Disarchive selfie anticolonialism website datasets SaferInternetDay content splinternet participation highavailability webstandards mapa groenlinks domains ntp centralized cloudfront socialnetworks metadata wikileaks disconnect Meme aioxmpp database socialanxiety proton disco web3 cloudfirewall TLSmastery descentralizare icmp organicMaps oop videocast governement jabber cleanuptheweb webbrowsers webhook communications decentralized userdata selflove wiki cloudron bsi browserextensions Fragattacks RedditDown ssb darknet cookies Qute MattHancock darkweb netcat webInstaller liberachat uberspace map Konfekoop Reddit archiv recaptcha server browser cloudy IPFS p2p social chainmaille antisocial tiddlywiki www missioncritical FreenodeTakeover corne fortinet Pluralistic databreach opendata ilovewikipedia web WebsiteStatus ownyourdata battiato netshutdowns alttext xep callforparticipation twitch im darkmode 9front bbb quadraticvoting GaiaX decentralise att jabberspam theserverroom antarctica shutdowns Watomatic datafree greenhost domain mesh selfemployed hackint OpenStreetMap gnusocial darkambient RudolfBerner slixmpp geminiprotocol statistics BurnermailIO irc eveonline pirate plaintext Graphika datacracy filesharing squatting misinformation rss openstreetmap ipns mozilla twitchbannedrevision voicemail gazaunderattack mapbox Nyxt legacyInternet yacy webrtc databases symbiotic debloattheweb crosspost jmap mail tinycircuits bureaucratic i2pd aesthetic ipfs internetradio bravenewworld routers practice browsers wikidata selfpub decentralizeit ballpointpen puredata netscape mixcloud DecolonizeTheInternet gmail openculture letthenetwork cyberspace SwitchToXmpp messaging selfies offthegrid enxeñeríasocial cloud ddg blabber snailmail cleanup selfdefense internet moderation decentralisation webinar metaverse qutebrowser _w3c_ socialcooling intox scholarsocial Seattle fox umap ssbroom pihole serverMeddling missingmaps qtox puremaps archiving bravesearch sneakernet NatureNeedsJustice Nextcloud internetarchive dataintegration mydataismydata dweb kmail js metatext adblock dark captcha socialNetworks BlackHatSEO beakerbrowser LiberaChat openweb soulseek NetShutdown enigmail libervia onlineharms webp gooddata mailinglist kernelupgrade dot Internet descentralizarea thepiratebay internetshutdowns fixtheweb mapporn contentid lazyweb atom kernel socialweb colonial AtomPub firewall shutdown ambient socialists kernenergie ebay mozillahubs instantmessaging publicservice interoperabilitate SolidProject webmention Justice4MohamudHassan cloudflare + homeserver datasette onlinesafetybill linkeddata markdown selfsufficiency webgl LoveWattsBLM decentralised immersiveweb pep decentraliseren i2p sceptic earlyinternet Clubhouse CooperativeClouds spam firefox redecentralize NYCMesh decentral socializing Burocratic toxicmasculinity staticsitegenerator wikipedia zeitschriften maps rtmp PlasticFreeJuly dataprotection NNCP decentralization inclusiónsocial decentralize IPFSing w3c OsmFinds datacollection files dotConism offlineimap DutchPolitics internetaccess agnostic gotosocial geminispace archivists gaza selfhosted piratenpartij mapuche videohosting DarkPatternsFTC metafilter maille meta wikibase CooperativeTechnology torrent mailab geocaching freenode MollyBrown mailfence bot adblocker tox k9mail nylasmail smalltech data socialism basemap webarchive sitejs meshroom protocol anticolonial VerkehrsswendeJetzt thecloud Jabbber worldbusterssocialclub publicserviceinternet networks criticism bioinformatics online openddata centralisation flameshot internetarchaeology WordPress darkages hiddenServices chainmail datarecovery self elinks saferinternetday selfhost text SeattleHellDay contentmoderation distributed OperationPanopticon mappe mydata webhosting decentralizedweb mailman SOASJusticeforCleaners natto p2pleft socialdistancing router sysadminday protection rne dataretention speedtest ublockorigin bigdata routeros internetofthings -} + +data greenhosting selfhosting forkawesome communityhosting TikTok tilde CriminalJusticeBill networking brave panopticon aldi icann selfsustaining hosting mailart DAOs discourse digitalcolonialism weblate kinosocial libera coopserver PeerToPeer wikis dns decentralizetheweb stripe service openstandards economíasocial responsiveness nojs ejabberd amusewiki freifunk oauth Anticon tic foxes hypercore CDNsAreEvil meshtastic piratebay protonmail TubEdu standards StuffCircuit yourdataisyourdata internetfreedom mirroring onlineWhiteboard gemini antarctic zeit webui InternetCrimeFamily wlan boilemMashEmStickEmInAStew internetBanking SmallWeb fedwiki snikket redessociales fleenode ircd coopcloud cw internetshutdown democratic criticalmass masculinity datadetox mailpile clearnet cdn cloudflared liberapay pinterest brahmaputra distributedcoop xmpp semanticweb identicurse socialnetwork Disarchive selfie anticolonialism website datasets SaferInternetDay content splinternet participation highavailability webstandards mapa groenlinks domains ntp centralized cloudfront socialnetworks metadata wikileaks disconnect Meme aioxmpp database socialanxiety proton disco web3 cloudfirewall TLSmastery descentralizare icmp organicMaps oop videocast governement jabber cleanuptheweb webbrowsers webhook communications decentralized userdata selflove wiki cloudron bsi browserextensions Fragattacks RedditDown ssb darknet cookies Qute MattHancock darkweb netcat webInstaller liberachat safety uberspace map Konfekoop Reddit archiv recaptcha server browser cloudy IPFS p2p social chainmaille antisocial tiddlywiki www missioncritical FreenodeTakeover ageverification corne fortinet Pluralistic databreach opendata ilovewikipedia web WebsiteStatus ownyourdata battiato netshutdowns alttext xep callforparticipation twitch im darkmode 9front bbb quadraticvoting GaiaX gavcloud decentralise att jabberspam theserverroom antarctica shutdowns Watomatic datafree greenhost domain mesh selfemployed hackint OpenStreetMap gnusocial darkambient RudolfBerner slixmpp geminiprotocol statistics BurnermailIO irc osm eveonline pirate plaintext Graphika datacracy filesharing sysadminlife ownlittlebitofinternet squatting sysadmin misinformation rss ipns mozilla twitchbannedrevision voicemail gazaunderattack mapbox Nyxt legacyInternet yacy webrtc databases symbiotic debloattheweb crosspost fastmail sysadmins jmap mail tinycircuits bureaucratic i2pd aesthetic ipfs internetradio bravenewworld routers practice browsers wikidata selfpub decentralizeit ballpointpen puredata netscape SSH mixcloud RSS DecolonizeTheInternet gmail openculture websites letthenetwork cyberspace SwitchToXmpp messaging selfies offthegrid enxeñeríasocial cloud ddg bopwiki blabber snailmail cleanup selfdefense internet moderation decentralisation justcloudflarethings webinar metaverse qutebrowser _w3c_ socialcooling intox scholarsocial Seattle fox umap centralization ssbroom pihole serverMeddling sealioning missingmaps qtox puremaps archiving bravesearch sneakernet NatureNeedsJustice Nextcloud internetarchive dataintegration mydataismydata dweb kmail js metatext adblock dark captcha socialNetworks BlackHatSEO beakerbrowser LiberaChat openweb soulseek NetShutdown enigmail libervia failwhale onlineharms webp gooddata mailinglist kernelupgrade dot wifi Internet descentralizarea thepiratebay internetshutdowns fixtheweb mapporn contentid lazyweb servers atom kernel socialweb colonial AtomPub firewall shutdown ambient socialists kernenergie ebay zeitschrift mozillahubs instantmessaging publicservice interoperabilitate SolidProject tiktok Justice4MohamudHassan cloudflare - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT + + + employment + justworked futureofwork InterviewQuestions jechercheunjob mywork remote employees hiring TheNetwork workingfromhome ProgrammingJob reproductivework frame workinprogress bullshitjobs car workplace DigitalNetwork antiwork workshops kreaturworks workers worklog sexworkers remotejob mainframe remotework remotejobs migrantworkers job culturalworkers DjangoJob teamwork framework hire KDEGear careers hirefedi career SocialNotworks workshop bedfordshire illustratorforhire OpenHospitalityNetwork tidyworkshops carework AtlasNetwork nowhiring KDE remoteaccess rds KDEGear21 obs workersrights obsolescence records KDEFrameworks plannedobsolescence work hertfordshire flossjobs jobs workflow precariousworkers carddav sexworker theworkshop nerdsnipe employee overwork + + Tue, 10 Aug 2021 08:34:29 UT + + + gafam + zuckerberg caringissharing ads apple youtuberegrets antitrust SpringerEnteignen peoplefarming deletewhatsapp advertisingandmarketing chromevox GoogleDown aws AppleSearch Floc bankruptBezos googlesearch googleio mycologists bringBunysBack youtube Goggle twitterkurds banadvertising chromebook fuckfacebook headset arcgis ffs FacebookEvents AmazonMeansCops facebook wandering 100heads 20thcenturyadvertising amazon googlevoracle amazonprimeday dystopia microsoftgithub farcebook myco boycottinstagram FlocOff stopgafam genoegisgenoeg legislation amazonprime deletewhatsappday amazonring Gafam googleplus soldering GoogleForms weirdyoutuberecomendations HaringeyAnti delete FoodSharing lobbyregister degooglisation florespondance linkedin siri Apple Facebook LeiharbeitAbschaffen PoweringProgress advertising monopolies googleanalytics ausländerzentralregister adtech fuckgoogle storing plottertwitter failbook kadse microsoft deletechrome alanturing dtm poledance HeadscarfBan twitter skype azure chrome logistics googledoodles hildebrandt twitterblue corporateGiant Tracking uitkeringen FlocOffGoogle sidewalk plot zuck nogafam youtubedl degoogled Google youtubers google Microsoft stemverklaring gis walledgarden GAFCAM dt GooglevsOracle dotcoms deleteyoutube datafarms Instagram walledgardens agistri appleevent offseting Hypnagogist appleii facebookoversightboard fascistbook FuckGoogle degoogle boringdystopia fuschia ohneamazon appleiie deleteinstagram ungoogled ring stopgoogle affordances googledown decentring YouTube gafam inspiring oracle killedbygoogle fuckoffgoogle dance deletefacebook gradschool fakebook GoogleIsBad fuckoffgoogleandco office365 lordoftherings turingpi amazonas instagram TrackingFreeAds FlocBloc playstore synergistic bigtech boycottamazon amazonien whatsapp mytwitteranniversary deleteamazon bluesky Amazon + + Tue, 10 Aug 2021 08:34:29 UT + + + people + Melissa harold paul Zachary JusticiaParaVictoria danielle dylan scott Barbara Kenneth theresa Denise FrankLeech louisrossmann Jesse Adam justin JonathanCulbreath elinorostrom katherine judith Karen Patricia russell Metalang99 juan diane Rebecca donna LouisRossmann olivia peter troy William denise NathanDufour Betty evelyn Christina brittany Jennifer Gregory Wayne Andrychów ethan Ralph Peter ecc americalatina jacobites jean laura betty nathan brownmark margaret alexanderlukashenko Bryan Virginia Jose Rose eric james BomberBradbury david Joshua christine haaland Billy CapitolRiot ostrom natalie daniel Jonathan Michael susan George johnny bookmark MichaelWood Lauren christina Amy kevin Natalie kenneth noahkathryn mannaggia Lawrence aaron donaldtrump gregory LindaLindas Amber alexa Robert Edward Patrick Rachel Verwaltunsgericht willemalexander bruce Forms dennis LegalCannabis Kayla frank KarenArmstrong Diane AliceHasters Donna Jack Paul Janice Brenda alexis sylvanasimons timothy vincent Alice sarah amy Daniel RobertKMerton jeff charlotte carolyn Emma Kyle Sean emily linda Olivia Eugene johnpilger Donald janet ryan Bookmarker stdavids RichardDWolff bryan DonnaStrickland Hannah anna doctorow MalcolmJohnson gretathunberg Catherine Alexander Christopher bob doris Anthony singlemarket Jean diana Beverly frances Sarah margaretthatcher Jordan peterrdevries JensStuhldreier Anna Ethan hackchrist Amanda jeremy donald NatashaAKelly mark matthew julie ryanair BenSchrader DrJessicaHutchings stephanie Jerry SEKFrankfurt Diana David Linda adam richard henry RoyalFamily Isabella elizabeth nachrichten steven jessica Walter dry jeffrey Kevin Justin mountanMaryland grace martinluther PeterGelderloos brandon mary anwarshaikh jamesbaldwin sharon nicholas Benjamin GeorgeFloyd amanda Emily Ruth heather stephenlawrence albert julianassange Julie marktwirtschaft nancy stephen Cannabis James CarlSpender Megan bettydog Raymond eugenetica michelle frankgehtran Nancy Fedimarket Frances Henry andrew kevinRuddCoup Jessica zurich IgorBancer julia marketing Dorothy BadVisualisation LoganGrendel Jason Charles JonathanMorris Danielle Brandon jose noamchomsky virginia beverly obituary ronald Bob BarbaraKay madison alberta ceph Helen MarkoBogoievski Jeff helen Sophia larry bookmarks dorothy Dennis JamesEPetts monbiot Nicholas Frank jack Stephen Janet ScottRosenberg georgemonbiot Alexis Pamela Jacqueline Dylan roy brenda jackal jesse Roger Jeffrey Brittany Shirley putkevinback Nathan christopher Carol Susan jason Philip Logan sandra jacob rose isabella Cynthia Joan jackieweaver aldoushuxley Maria martha Randy SarahEverard carl kyle karen raymond alice jerry carol RussellBrown Victoria Steven Douglas Lisa JonathanZittrain Julia joshua jacqueline Ashley assange eugene Bruce Albert Austin thomas Evelyn Gary Scott kimberly lawrence virgin jennifer Russell austin erdogan betterlatethannever ShhDontTellJack logan Laura Chris walters Teresa GeorgeGalloway Aaron Keith brian marktwain maryanning LamySafari maria Joseph Andrew Vincent Katherine Joyce NathanJRobinson lauren Ryan amber davidgraeber UrsulaFranklin alan ralph princephilip DennisTheMenace megan Kathleen sophia Cheryl abigail cynthia john richardstallman Alan AnnihilationOfCaste Debra GeorgeHoare arthurgloria mariadb LouisFurther Christine marilyn anthony chris Berichte Elizabeth sean Louis Larry AnnSophieBarwich christian deborah billy Abigail joesara AndreaBeste keith Jeremy CapitolRiots markkennedy zachary ruth Grace teresa Doris benjamin Willie george PeterHitchens methane barbara scottish Charlotte philip DaveCunliffe ethanzuckerman randy Margaret Heather Bradley Jacob shirley pamela Matthew Nicole joan judy Kelly savannah Brian melissa Sandra stallman markstone joseph oberverwaltungsgericht andrea shamelessselfplug Joe Sara robert alicevision aaronswartz better Bobby emma willie william angela rich SachaChua samuel Postmarketos tyler Thomas John kroger patricia ashley bobby roses kelly fuckamerica ThomasCahill hannah Carolyn Ann CrimsonRosella Jeangu gary wayne DavidRose Marilyn Deborah christenunie rms Sharon gare Mary frankfurt Samuel BreonnaTaylor Mark walter rebecca RaymondHill helendixon Madison Juan lisa cheryl janice ChristopherTrout jeffreyepstein Christian gerald Timothy roger edward bradley Gerald PiersMorgan patrickrachel framalang Kimberly steve Gabriel Marie EmmaFerris PeterHoffmann PaulBaran louis kathleen Arthur Gloria terry royals freejeremy bernardhickey Richard jonathan Harold shame Roy samantha DavidSeymour Carl chalice Eric AndreiKazimirov RebeccaHarvey relationships visuallyimpaired nicole Andrea Judith Terry Stephanie Johnny Angela Noah Kathryn RichardBoeth Ronald AskVanta Michelle Theresa gabrielmarie Samantha Judy michael charles GeorgeGerbner Tyler philipmorris amaryllis DouglasPFry kayla catherinealexander Martha debra JohnMichaelGreer stevewozniak joyce + + Tue, 10 Aug 2021 08:34:29 UT + + + activitypub + followerpower FederatedSocialMedia mastodevs kazarma activitypub activertypub FediTips tootfic askthefedi fedivision pleroma losttoot Rss2Fedi PeerTube CreativeToots devices gofed getfedihired collaborate pixelfedlabs hometown homelab RedactionWeb fediblock fediverso lazyfedi happyfedi2u federation Invite2Fedi instances fedilab bandsofmastodon Wallabag blocks pixiv mastotips TheFediverseChallenge sammelabschiebung toot fedilabfeature mastodev fediversetv pixel Ktistec mastodontips catsofthefediverse misskeydev mastotip pixel3a wallaby MastoDev friendica mastodontip talesfromthefediverse mastofficina fleamarket ap_c2s hiveway bands mastodonart mast Moneylab Mosstodon Adblocker fedionly DeveloperExperience askthefediverse misskey collaboraoffice activitypub_conf plsboost BlackFedi joinmastodon AskPixelfed siskin socialhub followers fediart blocking fedifreebies Metatext FediBlock SocialMediaReimagined fediverse13 mondkapjesplicht Pixelfed contentwarnings pixelfed labournettv fediverseplaysjackbox mapeocolaborativo fedihive greeninstances fedidb block FediMemories mastectomy Feditip devs fablab fediverseparty collabathon Dev Fediseminar onlyfedi admin socialcg teamtoot masterton fedbox FediMeta sponsorblock SocialNetworkingReimagined tusky retoot contentwarning peertubers imagedescription joinpeertube anastasia feditips tootcat dnsssecmastery2e fedizens alimonda Mastodon following epicyon afediversechat andstatus peertubeadmin leylableibt fediversefleamarket mastomagic YearOfTheFediverse dearMastomind thatsthetoot mastodob fediadmin pleaseboost mastodonhost mond pixeldev pixelfont timeline socialmedia tips wedistribute fedivisionCollab fosstodon instanceblock softwaredevelopment freetoot mastodonmonday fedihelp fediWhen fedicat asta collaborative isolategab greenmastodon FediverseFixesThis fedireads pixeldroid networkTimeline PeertubeMastodonHost boost AskFediverse Bookwyrm federated socialhome greenfediverse WriteFreely fédiverse microblocks collabora fedivers MastodonMondays fediverse imagedescriptions mastobikes gbadev lemmy Fedilab bunsenlabs mastoadmin smithereen hackerstown uadblock c2s FediverseFutures latenighttoots mastodon pcmasterrace developingcountries boostswelcome PixelfedDev fedi fediversefriday mondkapje fediplay activity widevine socialcoop peertube fieldlabs mastomind lab fediversepower BlackMastadon fedeproxy boosten tootorial boostwelcome lazyfediverse mastoaiuto mobilizon Fediverse13 lazy gemifedi activityPubRocks + + Tue, 10 Aug 2021 08:34:29 UT linux - pubnix linuxboot compiz osdev musescore commandline opensuse share linuxisnotanos elementaryos cli buster viernesdeescritorio voidlinux shell nu cliff olinuxino deb composite beschbleibt kde FragAttacks Debian11 reprobuilds pureos nospoilers kdepim thisweekinlinux slackware search bsd tap openwrt falling runbsd distros stapler tmux nixos alpine nix DebianBullseye rm xfce ubuntubuzz gnutools vaguejoke ack shareyourdesktop shellagm personal wireguard posix lightweight whonix hardenedbsd linuxaudio mate haikuos usb nushell LinuxTablets nixpkgs wordsearch landback osi alpines computertruhe nonmateria torvalds gtk linuxmint DebianAcademy debian chroot trisquel studio gnome distrowatch linuxposting fedoraonpinephone trackers console showyourdesktop FuckDeMonarchie researchassistants anarchie windowmanager desktop GuixSystem arch personalities platform ubuntu personalwiki jodee snowfall gnulinux patriarchat aur tuxjam justlinuxthings xubuntu kdeframeworks5 stackoverflow unix fedora openbsd centos nos fittrackee tuxedocomputers tracker openmandriva backwaren gentoo buildroot aurora architecture researcher BlackLives liveusb dee SearchFu personalarchive usergroup StockOS systemd linuxgaming Debian distro 1492LandBackLane Racklet theartofcomputerprogramming icecat tape puppylinux destinationlinux LinuxSpotted lovelinux thestudio suicide show Squarch monstrosities computer gtk3 blackout deepBlah escritoriognulinux acepride qubesos i3wm clipstudiopaint dadjokes kubuntu epr JuiceFS reproducible kdecommunity haiku alpinelinux linuxisnotaplatform clip fall linux EMMS planetdebian minicomputer altap raspbian netbsd DanctNIX termux btrfs reproduciblebuilds showTheRainbow gravitationalwaves joke artix gtk4 esc linuxexpress archlinuxarm bash exposingtheinvisible archlinux hare ubuntucore linuxconfau researchers AuratAzadiMarch gnomebuilder GNUlinux rhel debianinstaller debianindia linuxisajoke tux devuan debían suse zsh linuxconsole scoobySnacks + pubnix fishshell linuxboot compiz osdev musescore commandline opensuse share linuxisnotanos elementaryos cli buster viernesdeescritorio voidlinux shell nu cliff olinuxino deb composite beschbleibt kde FragAttacks Debian11 reprobuilds pureos nospoilers kdepim thisweekinlinux slackware search bsd tap openwrt falling runbsd distros stapler viernes tmux nixos alpine nix DebianBullseye jobsearch rm xfce ubuntubuzz gnutools vaguejoke ack shareyourdesktop shellagm personal wireguard posix lightweight whonix hardenedbsd Guix linuxaudio mate haikuos usb initramfs nushell LinuxTablets nixpkgs wordsearch landback osi alpines computertruhe nonmateria torvalds gtk linuxmint DebianAcademy debian chroot trisquel studio gnome distrowatch oldcomputerchallenge linuxposting fedoraonpinephone trackers console showyourdesktop FuckDeMonarchie researchassistants anarchie windowmanager desktop GuixSystem arch chaoscomputerclub personalities platform ubuntu personalwiki jodee snowfall gnulinux patriarchat aur tuxjam justlinuxthings xubuntu thesuicidesquad kdeframeworks5 stackoverflow unix fedora openbsd centos nos fittrackee tuxedocomputers tracker openmandriva backwaren gentoo buildroot aurora researcher archive icarosdesktop BlackLives liveusb dee SearchFu personalarchive usergroup StockOS systemd linuxgaming Debian distro 1492LandBackLane Racklet theartofcomputerprogramming icecat tape puppylinux destinationlinux LinuxSpotted lovelinux thestudio suicide aros show Squarch monstrosities computer gtk3 blackout deepBlah escritoriognulinux acepride materials qubesos i3wm clipstudiopaint dadjokes kubuntu epr artixlinux JuiceFS reproducible kdecommunity haiku alpinelinux linuxisnotaplatform clip fall linux EMMS planetdebian minicomputer altap raspbian netbsd DanctNIX termux btrfs reproduciblebuilds showTheRainbow gravitationalwaves joke artix gtk4 esc linuxexpress archlinuxarm bash dd exposingtheinvisible archlinux hare ubuntucore linuxconfau newinbullseye researchers AuratAzadiMarch gnomebuilder void GNUlinux rhel debianinstaller debianindia linuxisajoke tux devuan debían suse zsh linuxconsole scoobySnacks bullseye - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT programming - Easer psychotherapie DigitalInfrastructure cpp digitalpreservation programming css maui rubyonrails objects Python system digitaldivide digitalisierung FrancisBacon2020 dracut gitea orgmode mixers webdev proofing developerexperience seguridaddigital gui digital release ada schutzstreifen pypi crust codeforge workaround proofofwork zorg node websocket proofofstake ecosystem rustlang systemwandel DigitalTech python2 ocaml NapierBarracks system76 program ngiforum21 DigitalSouveräneSchule request_reaction sqlite guile capitolhillautonomouszone transcript TransZorgNu nim uptronics algorithmicharm hypocritcal profiles digitalsketch DeutschlandDigitalSicherBSI typescript forums vscode aapihm gitsyncmurder musicforhackers publiccode ocr computerscience hackers guidelines vieprivée Digitalzwangmelder laravel vala adventofcode cgit solidarność DigitalPayments beginnersguide CommonJS webdev101 scripting coding warn mauikit digitalesouveränität DevelopmentBlog anime ohShitGit digitalzwang meld git org QR_code proof sourcehut ui nocode solid nodejs systemchange trevornoah zinccoop tailwindcss terminalporn Wassersouveränität guix libertàdigitali js_of_ocaml raku fedidev c script freenode-services sourcecode publiekecode framaforms WendyLPatrick DigitalAutonomy grep django gmic zim sackthelot amada gitportal Acode gitlab crusty decoder bulldada readability parrot relevance_P1Y mnt digitalartwork Verkada react kingparrot Leiharbeit programmer trunk java haskell OpenSourceHardware CodedBias codelyoko workstation guixhome Tarifvertrag capitolhill desperatehousehackers esm penguin unicode development gittutors ursulakleguin gerrit db frgmntscnr Fagradalsfjall dev github freecodecamp openrc tuskydev threema recoverourdigitalspace html5 algorithms PythonJob lisp digitaldefenders codeberg souveränität forge ursulaleguin pleaseshare rustprogramming EspacioDigital HirsuteHippo resnetting frontenddevelopment animatedgif fourtwenty rails rakudev adaptation programme developers bug fortran libraries drivers animation printingsystems freecode forgefed javascript fragment cpm code elisp JardínOpenSource commands patterns eq ECMAScriptModules html codeofconduct vintagecomputers ConstructiveAmbiguity rakulang portal terminal c99 SemillasOpenSource rust programminghumor lowcode request AreWeTheBorg spiritbomb r FOSSlight bugbounty dramasystem go forges digitalaudioworkstation esbuild federadas commonlisp golang clojurescript vintage ruby releaseday rustc contractpatch rubylang dd deceptionpatterns mugorg debugging makejavascriptoptional nodefwd obsolescence_programmée computers developer darkpatterns racket sourceforge forum ksh digitalprivacy minimumwage bugreport mercurial aapi adafruit openappecosystem python fontforge webdeveloper indiedev ocrodjvu sh digitalGardens api assembler kabelfernsehen OpenSource Scheibenwischer + Easer psychotherapie DigitalInfrastructure cpp digitalpreservation programming css maui rubyonrails objects Python system digitaldivide digitalisierung FrancisBacon2020 dracut gitea orgmode mixers webdev proofing developerexperience seguridaddigital gui digital release ada schutzstreifen pypi crust codeforge workaround proofofwork zorg node websocket proofofstake ecosystem rustlang systemwandel DigitalTech python2 ocaml NapierBarracks system76 program ngiforum21 DigitalSouveräneSchule request_reaction sqlite guile capitolhillautonomouszone transcript TransZorgNu nim warsawhackerspace uptronics algorithmicharm hypocritcal profiles digitalsketch DeutschlandDigitalSicherBSI typescript forums vscode aapihm gitsyncmurder musicforhackers publiccode ocr computerscience hackers guidelines vieprivée Digitalzwangmelder laravel vala adventofcode cgit solidarność DigitalPayments beginnersguide CommonJS webdev101 scripting coding warn mauikit digitalesouveränität DevelopmentBlog anime ohShitGit digitalzwang meld git org QR_code proof sourcehut ui nocode solid nodejs systemchange trevornoah zinccoop tailwindcss terminalporn Wassersouveränität guix libertàdigitali js_of_ocaml raku fedidev c script freenode-services sourcecode audiodescription publiekecode framaforms WendyLPatrick DigitalAutonomy grep django gmic zim sackthelot amada gitportal Acode gitlab crusty decoder bulldada readability parrot relevance_P1Y mnt digitalartwork Verkada react dogfooding webdevelopment kingparrot Leiharbeit programmer trunk java haskell OpenSourceHardware CodedBias codelyoko workstation guixhome Tarifvertrag capitolhill Auto desperatehousehackers esm penguin unicode development gittutors ursulakleguin programminglanguage gerrit db frgmntscnr Fagradalsfjall dev github freecodecamp openrc tuskydev threema recoverourdigitalspace html5 algorithms PythonJob lisp digitaldefenders codeberg souveränität forge ursulaleguin pleaseshare rustprogramming EspacioDigital HirsuteHippo resnetting frontenddevelopment animatedgif fourtwenty rails rakudev adaptation programme developers bug fortran libraries drivers animation printingsystems freecode forgefed javascript fragment cpm code elisp JardínOpenSource commands patterns eq ECMAScriptModules html codeofconduct vintagecomputers ConstructiveAmbiguity rakulang portal terminal c99 SemillasOpenSource rust programminghumor lowcode request AreWeTheBorg spiritbomb r FOSSlight bugbounty dramasystem go forges digitalaudioworkstation esbuild federadas commonlisp golang clojurescript nodemcu vintage ruby releaseday rustc contractpatch rubylang deceptionpatterns mugorg debugging makejavascriptoptional nodefwd obsolescence_programmée computers developer darkpatterns racket sourceforge forum ksh digitalprivacy minimumwage bugreport mercurial aapi adafruit openappecosystem python fontforge webdeveloper indiedev ocrodjvu sh digitalGardens api assembler kabelfernsehen OpenSource Scheibenwischer - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT legal - NoALaReformaTributaria eek scanlines kurmancî rma informatik formatie2021 hfgkarlsruhe doj amro karlsruhe dmc remotelearning tamron formatie SpreekJeUitBekenKleur newnormal line disinformation kurmanji OnlineHarms GameSphere squeekboard mermaid stopline3 DNSmugOfTheWeek permagold OnlineHarmsBill laipower gdpr intros Anticritique energyflow peekier MovieGeek OnlineMeetings informationsfreiheit mojeek digitalservicesact line3 disinfo mainline darmanin airline OfflineHarms permafrost geekproblem dmca + NoALaReformaTributaria justafewlines eek scanlines kurmancî rma informatik formatie2021 hfgkarlsruhe doj amro karlsruhe dmc remotelearning tamron formatie SpreekJeUitBekenKleur newnormal line OfflineNavigation disinformation kurmanji OnlineHarms GameSphere squeekboard mermaid stopline3 DNSmugOfTheWeek permagold OnlineHarmsBill laipower gdpr intros Anticritique energyflow peekier MovieGeek OnlineMeetings scan informationsfreiheit mojeek digitalservicesact line3 disinfo mainline freiheit darmanin airline OfflineHarms permafrost geekproblem dmca - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT nature - hiking camping RedNeckedWallaby reforestation hillwalking wat hambach nsu20 marsupial lightning StormBella zensurheberrecht insect morning lavawervelwind seashell delightful plankton trees sky_of_my_window lichen MicroOrganisms badger nsu2 ProForestation nonsupremacy light gecko birds nature embargo_watch volcano teamcapy butterflies Nature frogs rainforest snow sunrise fossils hambacherforest forestfinance lighthouse hitchhiking leopardgecko moutains coldwater rocks inaturalist clamfacts sunset naturereserve forest LandRestoration australianwildlife forests capybara rgblighting enlightened waterfall sundaymorning forestation enlightenment natur lightening finance walking watches deforestation desert lava natural WoodWideWeb birdsarentreal lichensubscribe morningwalk lighttheme nsu retraction_watch SpringRockShed insects wildlife GreatInsults afforestation northernlights RainforestAlliance ProtégeonsLaNature amphibians walk desertification otter + hiking camping RedNeckedWallaby reforestation hillwalking wat hambach nsu20 marsupial lightning StormBella zensurheberrecht insect morning lavawervelwind seashell delightful plankton otterbox trees sky_of_my_window lichen MicroOrganisms badger nsu2 ProForestation nonsupremacy light gecko birds nature embargo_watch volcano teamcapy butterflies Nature snowden actiblizzwalkout frogs rainforest snow sunrise fossils hambacherforest forestfinance lighthouse hitchhiking leopardgecko moutains coldwater rocks inaturalist revuestarlight clamfacts sunset naturereserve forest LandRestoration australianwildlife forests capybara rgblighting enlightened waterfall sundaymorning forestation enlightenment natur lightening finance walking watches deforestation desert lava natural WoodWideWeb birdsarentreal lichensubscribe morningwalk lighttheme nsu retraction_watch SpringRockShed insects wildlife GreatInsults snowdrift afforestation northernlights RainforestAlliance ProtégeonsLaNature amphibians Bear walk desertification otter - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT writing - blog framablog interactive amwriting authors writingprompt poem lime cutupmethod story pdf blogPages swap shortstory prompts magazine smallstories prompt blogging smallpoems sciencefiction responsetootherblogs writing proverbs quotes blogs teleprompters noblogo otf logo playwright hedgedoc interactivestorytelling westernjournal Videopoetry quote olimex QuickSummary letterwriting icanhazpdf microblog bulletjournal storytelling goodread goodreads journalist creativewriting horror wordplay writers limerick journals artjournaling zineswap zines shortstories journalists journal writingcommunity poetry 20thcenturypoetry amwritingfiction + blog framablog interactive amwriting authors writingprompt poem lime cutupmethod story pdf linkblog blogPages swap shortstory prompts magazine smallstories prompt blogging smallpoems sciencefiction responsetootherblogs writing proverbs quotes blogs teleprompters noblogo otf logo playwright hedgedoc FediWriters interactivestorytelling westernjournal AuthorsofSocialCoop Videopoetry quote olimex QuickSummary letterwriting icanhazpdf microblog bulletjournal storytelling goodread goodreads journalist creativewriting horror wordplay writers limerick journals artjournaling zineswap zines shortstories journalists journal writingcommunity poetry 20thcenturypoetry logos amwritingfiction - Tue, 13 Jul 2021 08:43:43 UT - - - music - LibreMusicChallenge musicprodution KobiRock iea travessiapelavida LaurieAnderson ics punk punkname cooperativetechnology ourbeats gas vollgasindiekrise indieweb musician cypherpunk rutasenemigas synthesizer daftpunk bootstrappable kenloach indiemusic collapseos meatpunks LibreGraphicsMeetup cipherpunk 20thcenturyjazz acousticguitar synthpop psychedelicrock steamlinux playingnow streetpunk loader hydrapaper bikepunks bandcamp mymusic pop countryrock musicians jamendo ipod skinheadmusic jam rap shoegaze mp3 steam indie steganography steampunk ldjam48 indieauthor composing folkrock perlligraphy nazipunksfuckoff Music strap EnvoieStopHashtagAu81212 anarchopunk eurovisionsongcontest biography musicmaking psychedelic thecure posthardcore vaporwave IndustrialMusicForIndustrialPeople Mixtip dubstep synthwave bootstrap princeday oi graphisme rave freemusic nowplaying hiphop hardcore frappuccino Musicsoft experimentalmusic nazi folk cp TravesíaPorLaVida spotify fedimusic ml bootstrapping webscraping elisamusicplayer funkloch musicbrainz eurovision lasvegas catsWithMusicalTalent eos90D soundcloud psicodelia frankiegoestohollywood gastropod PigTrap bassguitar collapse 20thcenturymusic powerpop vinyl rock ccmusic denachtvanjanssen typographie dj newwave dorkwave producing experimental celticmetal prince musicproduction chiptune scraping loa Schleprock thrash bluestacks lastfm uploadfilters tekno ripprince Eurvision maunaloa technocracy asus 1 funkwhale 20thcenturyrock eos wp playlist retrosynth NowPlaying contest libremusicproduction psychrock MusicAdvent poppy coinkydink appropriatetechnology toledo samensterk indiepop rockalternativo MusicTouring indierock pmbootstrap midi arianagrande indiecember synth guitar blues musiciens listeningtonow abandonedplaces music folkpunk np bass techno gmtkjam musicmonday jazz production graphics dieanstalt perl darkwave mastomusic band TheGrunge metal chipmusic graphviz tigase polychromatic funk mindjammer popos magnatune fediversemusic pegasus grunge postpunk punkrock indieauth cyberpunkmusic raveculture cleantechnologies ldjam ftp BandcampFriday elisa mixtape garagerock MusicsoftDownloader camanachd - - Tue, 13 Jul 2021 08:43:43 UT - - - gardening - seedstarting BlagueDeCodeur sporespondence blockade inde mastogarden kinder communitygardening som deno composting soil sehenswert cabbage bundeswehr opensourceseeds onions lettuce blossoms gardenersofmastodon datenschleuder florespondence garten rinder succulent mulberry cherryblossoms garden thyme flower horticulture DailyFlowers Schlachthofblockade cherryblossom acu vegetable plant bricolage financialindependence kinderbijslag permaculture awesome teracube hens papuamerdeka Auflagen lag independenceday CompanionPlanting vlag gardens independence flowers seed kale seedvault plants thegardenpath devilslettuce vegetables thegarden fahrräder gardenersworld recyclage golden beekeeping toeslagenaffaire seeds Opensourcegarden toeslagenschandaal vegetablegarden - - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT countries - thai romania korean burma lithuania solomon chile europeanparliament Instanz boycottisrael fiji tajikistan benin paraguay eeuu icelandtrip senegal ukraine italy brunei nicaragua guyana Pflanzenbestimmung grönland euphoria zambia PalestineStrike iceland europeancouncil morocco netherlands swaziland EuropeanUnion bosnian suriname winningatlife elsalvador russia freeburma samoa StaatstrojanerParteiDeutschlands romanian asl european czech belarus hayabusa2 bw kyrgyzstan english uk translation sanmarino catalonia panama africa west indians unitedkingdom japan Netherlands buyused venezuela gambia freeNukem kuwait barbados papua greece switzerland brasilien uae mau england FuckIsrael nigeria usa angola honduras djibouti laos sierraleone nonprofit britain cambodia translators ych vietnam esperanto neofeud zealios seychelles marshall kazakhstan estonia investigate tonga stlucia burundi bangladesh egypt nachhaltigkeit japanese mali congo us IcelandicVerminControl jordan MusiciansForPalestine americangods digitaleurope speedrun grenada israel psychic algeria ghana bosnia translations russian eritrea bhutan armenian hama hungary Störungsverbot saudi slovenia tig czechosvlovakia bahamas libadwaita australia kiribati togo koreanorth poland Überbevölkerung ethereum malawi AlwaysBetterTogether capeverde armenia american hautrauswasgeht bahrain mozambique moa WichtigerHinweis abcbelarus japaneseglare americanpinemarten beleuchtung southsudan adminlife citylife europehoax Martesamericana syria german micronesia maldives iran indigenous sweden bijîberxwedanarojava ethiopia sid cuba liberia canada burkina indian Südwestgrönland somalia Chile whatshappeningintigray scotland Enlgand russiaToday vaticancity easttimor austria EuropeanUnionNews turkey yemen Bolivia denmark USBased domesticabuse austrianworldsummit madagascar finland Wales philippines ivorycoast haiti ecuador Portugal azerbaijan gasuk spain albania massachusetts afghanistan europe mauritania dominica ökonomisierung thailand belize westpapuauprising nerdlife macedonia montenegro ChileDesperto thenetherlands qatar mongolia costarica boatingeurope birdsofkenya boat latvia uzbekistan fatigue kabelaufklärung ireland iraq malaysia mexico investigations mauritius dezentralisierung oman chad nz de georgia zimbabwe france serbia lesotho romani halflife oddmuse tunisia argentina czechia cameroon namibia sudan indonesia lifeboat colombia worldwildlifeday kryptowährung tuvalu britainology merica beckychambers turkmenistan tanzania germany neuhier norway comoros auteursrecht guatemala Thailand kosovo eastgermany andorra wales indiastrikes vanlife Palestine servus pakistan belgium china 3615malife antigua life europeanvalues koreasouth newzealand visiticeland einzelfall rwanda luxembourg libya indywales italyisntreal nauru moldova bad spanish eastindiacompany northernireland stigmergic palau taiwan kenya trinidad eu botswana Lebensmittelzusatzstoff CuriosidadesVariadas jamaica vanuatu cyprus aminus3 israele malta Icelandic psychedelia niger s3 westpapua busse unitedstates myanmar saintvincent guinea nepal peru uganda uruguay india pacificnorthwest lebanon neurodiversity southafrica writer arte croatia europeanunion writerslife bolivia chinese dominican europeancommission srilanka bulgaria slovakia speedrunning gabon psychedelicart ether stkitts liechtenstein saveabkindonesia neofeudalism surinam brazil shutdowncanada + roma thai romania korean burma lithuania solomon chile europeanparliament Instanz boycottisrael fiji tajikistan benin paraguay eeuu icelandtrip senegal ukraine italy brunei nicaragua guyana Pflanzenbestimmung grönland euphoria zambia PalestineStrike iceland europeancouncil morocco netherlands swaziland EuropeanUnion bosnian suriname welcome2america winningatlife elsalvador russia freeburma samoa StaatstrojanerParteiDeutschlands romanian asl european czech belarus hayabusa2 bw kyrgyzstan english uk translation sanmarino catalonia panama africa west indians unitedkingdom japan Netherlands buyused venezuela gambia freeNukem kuwait barbados papua greece switzerland brasilien uae mau england FuckIsrael nigeria usa angola honduras djibouti laos sierraleone nonprofit investigation artemis britain cambodia translators ych vietnam esperanto neofeud zealios seychelles marshall kazakhstan estonia investigate tonga stlucia burundi bangladesh egypt nachhaltigkeit japanese mali congo us IcelandicVerminControl jordan MusiciansForPalestine americangods digitaleurope speedrun grenada israel lowestoft psychic algeria ghana bosnia translations russian LateAmericanSentences eritrea bhutan armenian hama hungary Störungsverbot saudi slovenia tig czechosvlovakia bahamas america libadwaita australia kiribati togo DeathToAmerica koreanorth poland Überbevölkerung malawi AlwaysBetterTogether capeverde armenia american hautrauswasgeht bahrain mozambique WichtigerHinweis abcbelarus japaneseglare americanpinemarten beleuchtung southsudan adminlife citylife europehoax Martesamericana syria german micronesia maldives iran indigenous sweden bijîberxwedanarojava ethiopia sid cuba liberia canada burkina indian Südwestgrönland somalia Chile whatshappeningintigray scotland Enlgand russiaToday vaticancity easttimor austria EuropeanUnionNews turkey yemen Bolivia denmark USBased domesticabuse austrianworldsummit madagascar finland Wales Iran philippines ivorycoast haiti ecuador Portugal azerbaijan gasuk spain albania massachusetts afghanistan europe mauritania dominica ökonomisierung thailand belize westpapuauprising nerdlife macedonia montenegro BelarusProtests ChileDesperto thenetherlands qatar mongolia costarica boatingeurope birdsofkenya Australia boat latvia uzbekistan fatigue kabelaufklärung ireland iraq malaysia mexico investigations mauritius dezentralisierung oman chad nz de georgia zimbabwe france serbia lesotho romani halflife oddmuse tunisia argentina czechia cameroon namibia sudan indonesia lifeboat colombia worldwildlifeday kryptowährung tuvalu britainology merica beckychambers turkmenistan tanzania germany trojan neuhier norway comoros auteursrecht guatemala Thailand kosovo eastgermany andorra wales indiastrikes vanlife Palestine servus pakistan belgium china 3615malife antigua life europeanvalues koreasouth newzealand visiticeland einzelfall rwanda luxembourg libya indywales italyisntreal nauru moldova bad spanish eastindiacompany northernireland stigmergic palau taiwan kenya trinidad eu botswana Lebensmittelzusatzstoff CuriosidadesVariadas jamaica vanuatu cyprus aminus3 israele malta Icelandic psychedelia niger s3 westpapua busse unitedstates myanmar saintvincent guinea nepal peru uganda uruguay india pacificnorthwest lebanon neurodiversity southafrica writer arte croatia europeanunion writerslife bolivia chinese dominican europeancommission srilanka bulgaria etherpad slovakia speedrunning gabon psychedelicart ether palestine stkitts liechtenstein saveabkindonesia neofeudalism surinam brazil shutdowncanada - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT + + + music + LibreMusicChallenge musicprodution KobiRock iea travessiapelavida LaurieAnderson ics punk punkname ourbeats gas vollgasindiekrise indieweb musician cypherpunk rutasenemigas synthesizer daftpunk bootstrappable kenloach indiemusic collapseos meatpunks LibreGraphicsMeetup cipherpunk 20thcenturyjazz acousticguitar synthpop psychedelicrock steamlinux playingnow streetpunk loader hydrapaper bikepunks bandcamp mymusic pop countryrock musicians jamendo ipod skinheadmusic jam rap shoegaze mp3 nettlepunk steam indie steganography PegasusSpyware steampunk ldjam48 indieauthor composing folkrock perlligraphy nazipunksfuckoff Music strap EnvoieStopHashtagAu81212 anarchopunk eurovisionsongcontest biography musicmaking psychedelic thecure posthardcore vaporwave IndustrialMusicForIndustrialPeople Mixtip dubstep synthwave bootstrap princeday oi graphisme rave freemusic nowplaying hiphop hardcore frappuccino Musicsoft experimentalmusic nazi folk cp TravesíaPorLaVida spotify fedimusic ml bootstrapping webscraping elisamusicplayer funkloch musicbrainz eurovision lasvegas catsWithMusicalTalent PegasusSnoopingScandal eos90D soundcloud psicodelia frankiegoestohollywood gastropod whenyoulistentocoildoyouthinkofmusic trial soundsynthesis PigTrap bassguitar collapse 20thcenturymusic powerpop vinyl rock ccmusic typographie dj newwave dorkwave producing experimental celticmetal prince musicproduction chiptune scraping loa Schleprock thrash bluestacks lastfm uploadfilters tekno ripprince Eurvision maunaloa technocracy asus 1 funkwhale 20thcenturyrock eos wp playlist retrosynth NowPlaying contest libremusicproduction psychrock MusicAdvent poppy coinkydink appropriatetechnology toledo samensterk indiepop rockalternativo MusicTouring indierock pmbootstrap midi arianagrande indiecember synth guitar blues musiciens listeningtonow abandonedplaces music folkpunk np bass techno gmtkjam musicmonday jazz production graphics dieanstalt perl darkwave mastomusic band TheGrunge metal chipmusic graphviz tigase polychromatic funk mindjammer popos magnatune fediversemusic pegasus PegasusProject grunge postpunk punkrock indieauth cyberpunkmusic raveculture cleantechnologies ldjam ftp BandcampFriday elisa mixtape garagerock MusicsoftDownloader camanachd + + Tue, 10 Aug 2021 08:34:29 UT + + + gardening + seedstarting BlagueDeCodeur sporespondence blockade inde mastogarden kinder communitygardening som deno composting soil sehenswert cabbage bundeswehr opensourceseeds onions lettuce blossoms gardenersofmastodon datenschleuder florespondence garten rinder succulent mulberry weekendGardeningThoughts cherryblossoms garden thyme flower horticulture DailyFlowers Schlachthofblockade cherryblossom agriculture acu vegetable plant bricolage financialindependence plasticflowersneverdie kinderbijslag permaculture awesome teracube hens papuamerdeka Auflagen wildflowers lag independenceday CompanionPlanting vlag gardens independence flowers seed kale seedvault plants thegardenpath devilslettuce vegetables thegarden fahrräder gardenersworld recyclage golden beekeeping toeslagenaffaire seeds Opensourcegarden toeslagenschandaal vegetablegarden + + Tue, 10 Aug 2021 08:34:29 UT privacy - privacyplease state whatip auditableprivacy PrivacyBook SearchHistory privacyaware dataprivacyday profiling what3words surveillancestate Privacy privacypolicy WhatsApp privacyrights privacytoolsio makeprivacystick privacyweek surveillancetech onlineprivacy developertools WhatMakesMeReallyAngry privacyredirect Liberanet LiberanetChat drugpolicy privacymatters policy privacyMatters whatsappprivacypolicy dataprivacy privacywashing privacy privacyinternational whowhatwere hat NoToWhatsApp DataPrivacyDay2020 PrivacyFlaw nl WhatsappPrivacy tool + privacyplease appleprivacyletter state whatip auditableprivacy appleprivacy PrivacyBook SearchHistory privacyaware dataprivacyday profiling what3words surveillancestate Privacy datenschutz privacypolicy WhatsApp privacyrights privacytoolsio privacyshield makeprivacystick privacyweek surveillancetech onlineprivacy developertools WhatMakesMeReallyAngry privacyredirect Liberanet LiberanetChat drugpolicy privacymatters policy privacyMatters whatsappprivacypolicy dataprivacy privacywashing fight4privacy privacy privacyinternational whowhatwere hat NoToWhatsApp DataPrivacyDay2020 investinprivacy PrivacyFlaw statePropaganda nl privacytools WhatsappPrivacy tool - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT + + + media + tradicional InfiniTime livestreaming ip digitalmedia mustwatch sustainable videobearbeitung transparency polarbears mediathek mianstreaming stream videoconferencias trad AtlanticGulfstream maistreaming ime sustainabilty mixxx shortfilm selfsustainable amstrad kawaiipunkstreams mainstream films streaming weAreAllCrazy video streamdeck puns maiabeyrouti videoconference shortfilms mix MediaEU mixed diymedia Fairtrade drmfree film streams massmedia stummfilm submedia theatlantic traditionalmedia videos Internetradio mediawatch mainstreamining newsmedia audiovideo videosynthesis filmnoir wikimedia railroad mixedmedia railroads heat documentary streamers artstream vi nationalgeographic folktraditions gstreamer tootstream taina ai mediawiki slowtv bear realmedia media independentmedia SiberianTimes theintercept + + Tue, 10 Aug 2021 08:34:29 UT + + + health + eventsourcing merchandise FreedomIsTheOnlyTreatment gnuhealth water 4 medical CoronaApp bundesregierung runningdownthewalls watersnood EfeLevent autism burnout Underunderstood cannabis hand event healthinsurance medicine anxiety freshwater mh inflammation run eternalpuberty NHSDataGrab treatment EmotionalFirstAid safeabortion4all maryjane organisierung autistic BlockBrunsbüttel running neurodivergent health motion crunchbang actuallyautistic meds PatientSafety marijuana suicideprevention mentalhealth postmortem H5N8 healthy DarrenChandler autismmeme einzelhandel drugs atm neurodiverse asperger cigarettes insurance hearingimpairment selfcare autismus + + Tue, 10 Aug 2021 08:34:29 UT hardware - plugandplay bluetooth printnightmare singleboardcomputer purism dating schematics opennic zomertijd librehardware BoBurnham restauration rmw riscv solarpower carbonFootprintSham mietendeckel PersonalComputer cyberdeck PineCUBE firmware tex keyboards debuerreotype electron ChromebookDuet AbolishFrontex bond hibernation PneumaticLoudspeakers schreibmaschine imac Nottingham schwarmwissen elitesoldat handheld screenless megapixels BibliothekDerFreien KeepTheDiskSpinning homebrewcomputing FarmersTractorRally pinebook farming modem lowtech biblatex allwinner datenschutz daten home pimeroni 68 lebensmittelsicherheit industrial hambibleibt analogcomputing homer TrueDelta keyboard screenprinting robotics Pinecil mutantC raspberrypi3 pocketchip oshw misterfpga noisebridge disapora T440p ArmWorkstation datensicherheit latexrun hardwarehacking mer picodisplay laptops electronics scuttlebutt ham teamdatenschutz charm SectorDisk wolnabiblioteka permacomputing uart panasonic pcb almere armbian performance kopimi printmaker deck making hambi powerpc solar ssd acoustics ibmcompatible webcams modular larp tweedekamer cybredeck latex 3dprinted emmc ipadproart computing laptop solarpunk isa recycling modularsynth apparmor repairability theatrelighting lenovo updates fairelectronics industrialmusic librem carbonsequestration electronica sed TokyoCameraClub MacBookProService pocket box86 JingPad righttorepair mac trackball fuse date solarpunkactionweek ibm 3dprinting electro carbon MechcanicalKeyboards netbook hardware m68k pisa retrohardware pinetab sicherheit openhardware raspberrypi irobot datenautobahn webtoprint 3dprinter barcode lüneburg Quartz64 PlanetComputer jtag ebu merseyside itsicherheit CompressedAirAmplification pinetime screens pinebookpro lebensmittel 3d batteries PinebookPro 3dprint pim Handprint modemmanager securescuttlebutt keyboardio mechanicalkeyboard electronicmusic solarpunks carbondioxide robot arm lowerdecks ipad FireAlarms PinePower paperComputer amd openpower poweredSpeaker devopa a64 eeepc bahn F9600 rpi4 thinkpad RaspberryPiPico iot dat BeagleV arm64 merveilles repairable sbc circuitbending raspberrypi4 print displayport akihabara analog electronic FrameworkLaptop + plugandplay bluetooth printnightmare singleboardcomputer purism dating schematics opennic tektronix zomertijd librehardware BoBurnham restauration rmw riscv solarpower carbonFootprintSham mietendeckel PersonalComputer cyberdeck PineCUBE firmware tex keyboards debuerreotype electron ChromebookDuet AbolishFrontex webcam bond hibernation PneumaticLoudspeakers schreibmaschine imac Nottingham schwarmwissen elitesoldat handheld screenless megapixels BibliothekDerFreien KeepTheDiskSpinning homebrewcomputing FarmersTractorRally pinebook farming modem lowtech biblatex allwinner daten home pimeroni 68 lebensmittelsicherheit industrial hambibleibt analogcomputing homer TrueDelta keyboard screenprinting robotics Pinecil mutantC raspberrypi3 pocketchip oshw misterfpga noisebridge disapora T440p ArmWorkstation datensicherheit latexrun hardwarehacking mer picodisplay laptops electronics scuttlebutt ham teamdatenschutz charm SectorDisk wolnabiblioteka preprint permacomputing uart panasonic pcb almere armbian performance kopimi printmaker deck making hambi powerpc solar ssd acoustics ibmcompatible webcams modular larp tweedekamer cybredeck latex 3dprinted MacBook emmc ipadproart computing laptop solarpunk isa recycling modularsynth apparmor repairability macbook theatrelighting pc lenovo updates fairelectronics industrialmusic librem carbonsequestration electronica sed TokyoCameraClub MacBookProService pocket box86 JingPad righttorepair mac trackball fuse date solarpunkactionweek ibm 3dprinting electro carbon MechcanicalKeyboards netbook hardware m68k pisa retrohardware pinetab sicherheit openhardware raspberrypi irobot datenautobahn webtoprint 3dprinter barcode lüneburg Quartz64 PlanetComputer jtag ebu merseyside itsicherheit CompressedAirAmplification pinetime screens pinebookpro lebensmittel 3d batteries PinebookPro 3dprint pim Handprint modemmanager securescuttlebutt keyboardio mechanicalkeyboard electronicmusic solarpunks carbondioxide robot arm lowerdecks sonic ipad FireAlarms PinePower paperComputer amd openpower poweredSpeaker devopa a64 eeepc bahn F9600 rpi4 thinkpad RaspberryPiPico iot dat BeagleV arm64 merveilles repairable sbc circuitbending raspberrypi4 print displayport akihabara analog electronic FrameworkLaptop - Tue, 13 Jul 2021 08:43:43 UT - - - security - zuluCrypt signalboost encrypt letsencrypt messengers BrowserHistory FlexibilizaciónResponsable autoritäreretatismus BlacksInCyber omemo autotomy saveanonymity alg onionrouting Installationsanleitung dataleak messenger foodinsecurity password keepassxc partyline cryptography party cybersecuritynews pipewire Installation cryptolalaland solarwinds bitwarden communityalgorithmictrust infosec gchq GemeinsamGegenDieTierindustrie mitm wireless castor repairing IHaveSomethingToHide fotografie passwords gif IronySec cryptowars anonym encryptioncan supplychainattacks UseAMaskUseTor anonymous cyberattack editors security tor comb e2e supplychain bruceschneier gigafactory vpn BlacksInCybersecurity ransomware toreador itsec dnssecmastery2e openssh factorio Reactorweg openssl backdoored spyware dorfleaks torx encryptionsts e2ee sequoia backdoor cryptotokens NSAmeansNationalScammingAgency stork conscientiousobjectors ed25519 torproject cryptomeanscryptography encryption 0day informationsecurity ssh misshaialert cybersec ox restore FormFactors crypto theObservatory autokorrektur giftofencryption foodsecurity kansascity auto signalapp anonymity automattic fotografía onionshare onion encryptionist kontor autofahrer infosecbikini autocrypt malware switchtosignal cloudsecurity corydoctorow RestoreOurEarth radiorepair algérie hexeditor distortions cryptographyisoverparty opsec keepass encryptionists TastySecurity cryptobros securitybyobscurity torsocks toronto nsa autorenleben schneier protonvpn trustissues InsecurityByObscurity yubikey nitrokey encrypted 1password openpgp pgpainless tatort ghibli afraleaks castor9 deletesignal prismbreak gpgtools autodidactic gpg automation fotopiastory equatorial sequoiapgp cybersecurity Tor CryptoWars signal noscript redaktor vector trust Torge Torfverbrennung sasl cryptoparty wire historia AllmendeKontor itsecurity websecurity foto pgp RobinHoodStore cryptomator signalmessenger openvpn datasecurity autorotate regulators anleitung leak drugstore encryptiost libresignal doctors securitynow storage tracking - - Tue, 13 Jul 2021 08:43:43 UT - - - science - engineering math politicalgeography epidemiology stemfie TranslateScience electrochemistry ethnology womeninstem archeology botany STEM biodiversity ocean stemgeenFVD linguistic anthro supercollider nextgeneration zoology linguistics climatology uprootthesystem oceans SolarSystems reasoning awk dna geography physics intergenerational archaeologist generalstreik geology ClinicalPsychology generationidentitaire economicanthropology Science SystemicRacism OpenScience corrosion research stemwijzer systemsmap bioengineering GotScience sistemainoperativo stemgeenPVV knowledge stemgeenVVD botanical dream dawkins ineigenersache psychogeography stemgeenVVS holo graphTheory deepdreamgenerator AnnualStatisticalReview trilateralresearch meterology botanicalart JA21 regenerative ScienceDenial biotech stemgeenJA21 psychology dreamtime pataphysics particles biology bughunting researching_research hunt pacificocean generation gene fossilhunting arthunt badscience mathematics chemistry muon processengineering paleontology oceanography stem anthropocene particlephysics nextgenerationinternet biomedical mechanicalengineering anthropology - - Tue, 13 Jul 2021 08:43:43 UT - - - photos - smartphonephotography nikon 90mm photography fujifilm rewild photogrammetry wildlifephotography wild affinityphoto photocló photo photographe photogrpahy photographer tokyocameraclub nikond90 photos macrophotography photoshop photographie photovoltaik seancephoto camera crops photomanager macropod uwphoto wildbiene macronie photographers cameras fossphotography phototherapie phonephotography myphoto rewilding naturephotography fediphoto picture wildfood macro intothewild streetphotography FujinonXF90mm wildcat photoreference crop phototherapy pictures - - Tue, 13 Jul 2021 08:43:43 UT - - - history - musichistory heirloom monarchs holocaust history arthistory makeinghistory History anarchisthistory indigenoushistorymonth gaminghistory womenshistorymonth NetworkingHistory blackhistory monarch computerhistory HistoryOfArt - - Tue, 13 Jul 2021 08:43:43 UT - - - software - beta borgbackup forms app freeUP1 freedombox windows edit nginx transclusion krebsrisiken proprietarysoftware freepalestin calibre misophonia fosshost postscript nota AAPIHeritageMonth freenet freebsd kc Framasoft tts E40 Flisol2021 invidious drm softwarelibero publicdomain ilovefreesoftware hydra readers StoryMapJS kubernetes openvms luca nodrm copyleft fossmendations happyauthor freedoom librespeed jami betatesting NottsTV libregraphics genossenschaft FuckOffZoom quicksy thunder whiteboard free docker softwarelibre opensourcehardware interoperability impression3d freesoftware gimp krebs backups foss matrix dinosaur mossad unfa weechat clapper designjustice thefreethoughtproject filesystems nextcloud translate wechat notmatrix gnupg lucaApp chats duplicati HappyLight opensourcesoftware permissionless compression openscad freeganizm uidesign softwaredeveloper neochat TabOrder searx ikiwiki prosody Linux FreeSoftware userresearch FlisolLibre2021 DisCOElements Audio rocketchat thanksfreesw libres webapps immers outreachy synapse API freelibre lyft freekirtaner nitter monitoring misogyny virtualbox ngi4eu discord reverseengineering whisperfish ee opensourcedesign vaporware opensource diaspora yunohost oss librelounge chickadee appstore dégooglisons littlebigdetails cabal conferencing cadmium libreboot musiquelibre mycroft devops kdeapplications owncast phabricator emacs freiesoftware FLOSSvol moss fluffychat dinoim impress writefreely videoconferencing bigbluebutton tile_map_editor email ngi esri chatapps HappyNewYear Eiskappe fossilfriday floss plugins softwaresuite frecklesallovertheshow graphic libresoftware softwareengineering mosstodon expandocat deltachat application uifail FOSS peatfree lucaapp GNOMECircle rockpro64 bittorrent palestinewillbefree penpot vlc zoom southasia tiling session diaspora0800 FriendofGNOME Senfstoffknappheit usability winamp opendesign obnam snap appim ProprietarySoftwareProblems pandoc Happy4thJuly freemumia write artificialintelligence blackcappedchickadee cryptpad software libretranslate OwnStream upstream maplibre slack Hummingbard hydrated emacslisp Element freeware DismantleFossilCompanies safenetwork asia jit SoftwareLibre zrythm gnu CTZN silicongraphics mumble grsync freecad drmfree telegram containers tails freeschool chatons blockchain windows11 irssi HabKeinWhatsapp information mcclim jitsimeet dedrm iso mutt librelingo freetibet WeAreAlmaLinux tilingwm sri design gameoftrees GnuLinuxAudio freegan freeriding freetool backup trueLinuxPhone ngio rotonde freetube jumpdrive GNU speechrecognition eurovison skydroid thunderbird sysadmin it sound alternativeto screenreader parler bison apps chat licensing fossasia inclusivedesign ethicalsoftware defectivebydesign metager digitalsustainability screenreaders sysadmins ZeroCool LINMOBapps freedombone uber obsproject librecast softwareheritage pittsburgh profanity delta Tankklappe doomemacs imageeditor ffmpeg fossandcrafts GNOME40 telesoftware love reboot opensourcegardens musique switchingsoftware OSM freesw agpl distribute magnifyingglass GNOME freeganizmniewybacza drive freesoftare AlmaLinux GreenandBlackCross strafmaatschappij freetillie distributedledger mattermost principiadiscordia blue LinuxPhones filesystem rocket ghostscript win10 Zoom tibet ComputerFolklore fossaudio elemental SocialCreditScores flisoltenerife libreops element platforms inclusive librelabucm engineer softwareNotAsAService ptp chatty lucafail informationwantstobefree softwareGripe nativeApp MatrixEffect culturalibre jitsi taintedlove flisol engineers dinosaurier wordpress SwitchToJami mongodb ux rsync libreoffice crossstitch Encrochat dino RainbOSM plugin xwiki tecc openoffice container discordia softwaredesign redeslibres ledger sounddesign palestine chatcontrol alternatives glimpse libregraphicsmeeting - - Tue, 13 Jul 2021 08:43:43 UT - - - conferences - FOSDEM2021 stackconf fossnorth debconf debconf21 FOSDEM talk fossdem FreedomBoxSummit apconf2020 schmoocon Aktionscamp realtalk persco penguicon2021 letstalkaboutyes summit confidenceTricks agm libreplanet SeaGL2021 confindustria confluence minidebconf edw2021 maintainerssummit rc3worldleaks rightscon StopStalkerAds SeaGL penguicon emacsconf MCH2021 flossconference LGM2021 conferences LibrePlanet defcon emfcamp flossevent askpinetalk bc conf talks defcon201 rC3 rC3World FOSDEM21 conference mozfest flossconf bootcamp apconf ccc persconferentie GeekBeaconFest rC3one smalltalk camp g7 C3 config penguicon2022 confy - - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT food - vitamind cake veganism teamviewer FoodHardship vanilla pankow margarine zwartepiet panthera dessert foils salsa caviar utopie brot theexpanse BellaSpielt cookery pietons Ôtepoti panther food cakecutting skillet teamgodzilla spiel Vegan liquor SoilSovereignty milk bolognese recipe foodporn yeast drinking VendrediPeanutsNouka plate waffle pansexual biscuit glaze omelette veganismo morel filet pastry wine woke Caribbeans hamburger juice unauthorizedbread Amazfish Avocados management sourdough gedankenspiel cagefree words MauriceSchuhmann nuts gras toast broth batter foodie breadposting spiele zerowaste haggis ketchup carrots go-nuts damnfinecoffee divoc seasoning mayo nowords MastoEats soup arpanet pan voc imateapot Anglefish potatoes mayonnaise vegan dish avocado spice keto bakery butterfly cooking teamhuman SailfishOS Trypanophobia AgentProvocatuer yogurt rok thecandycrystalrainbowcodex crumble PropaneSalute cider caffeine Kinipan butter mastokitchen triceratops cook pottery kurdish creepypasta wastemanagement kitchencounter mastocook cobbler steak pizza vocaloid crystal soda fedikitchen aroma oil Miroil flour foodsovereignty cream nutella pie cut cuisine potse meatismurder freerange tartar kropotkin tea marinade cakes mushroom thekitchen govegan entree lfi dominospizza bread salad beans mush fresh syrup fermentation teamsky mushrooms cookie cookiebanners wordstoliveby curd soysauce lowcarb pudding plantbased beer organicfood peterkropotkin fish grasslands panoptykon spanisch honeypot foodnotbombs foodwaste organic wholeGrain wheat pot TeamFerment timewaster Wypierdalaj sauerkraut stew weltspiegel chocolate paste soynuevo wok rainbow recipes kitchengarden expanse olive burger mrpotatohead candy lifesnacks Steam kitchen coffee foodshortage bagel batterylife OpTinfoil teams taste SpieleWinter2020 meat johannisbeeren noodle raclette caramel rice eggs grill davewiner poutine demoteam lard croissant pasta vegane strawberry toomuchcaffeine morelmushroom foods coffeeaddict WaterDrinkers cheese oregano drink muffin bikekitchen krop LowRefresh kyotocandy foie onepiece sauce foodanddrink soy foodpics growyourfood vore mushtodon wholewheat pandemie cocoa sandwich bigoil mousse waste chili redfish + battery vitamind cake veganism teamviewer FoodHardship vanilla pankow margarine zwartepiet panthera dessert foils salsa caviar utopie brot theexpanse BellaSpielt cookery pietons Ôtepoti panther food cakecutting skillet teamgodzilla openfoodnetwork spiel Vegan liquor SoilSovereignty milk bolognese recipe foodporn yeast drinking VendrediPeanutsNouka plate waffle pansexual biscuit glaze omelette veganismo morel filet pastry wine woke Caribbeans hamburger juice unauthorizedbread Amazfish Avocados management sourdough gedankenspiel cagefree words MauriceSchuhmann nuts gras toast broth batter foodie breadposting spiele zerowaste haggis ketchup carrots go-nuts damnfinecoffee divoc seasoning mayo nowords MastoEats soup arpanet SteamDeck pan voc imateapot Anglefish mayoverse potatoes mayonnaise vegan dish avocado spice keto bakery butterfly cooking teamhuman SailfishOS Trypanophobia AgentProvocatuer yogurt rok thecandycrystalrainbowcodex crumble PropaneSalute cider caffeine Kinipan butter mastokitchen triceratops cook rain pottery kurdish creepypasta wastemanagement kitchencounter mastocook cobbler steak pizza vocaloid crystal soda fedikitchen coffeebreak aroma oil Miroil kochbrothers flour foodsovereignty cream nutella pie cut cuisine potse meatismurder freerange tartar kropotkin tea marinade cakes mushroom thekitchen govegan entree lfi dominospizza bread salad beans mush fresh syrup fermentation teamsky mushrooms cookie cookiebanners olivetti wordstoliveby curd soysauce lowcarb pudding plantbased tema beer organicfood peterkropotkin fish grasslands panoptykon spanisch honeypot foodnotbombs foodwaste organic wholeGrain wheat pot TeamFerment timewaster Wypierdalaj sauerkraut stew weltspiegel chocolate paste soynuevo wok rainbow recipes kitchengarden expanse olive burger mrpotatohead candy lifesnacks Steam kitchen coffee foodshortage bagel batterylife OpTinfoil teams taste SpieleWinter2020 meat johannisbeeren noodle raclette caramel rice eggs grill davewiner DavePollard poutine demoteam lard croissant pasta vegane strawberry toomuchcaffeine morelmushroom foods coffeeaddict WaterDrinkers cheese oregano drink muffin bikekitchen krop LowRefresh kyotocandy foie onepiece sauce foodanddrink soy foodpics growyourfood vore mushtodon wholewheat pandemie cocoa sandwich bigoil mousse waste chili redfish - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT + + + security + zuluCrypt signalboost encrypt letsencrypt messengers autos BrowserHistory FlexibilizaciónResponsable puntarellaparty autoritäreretatismus BlacksInCyber omemo autotomy saveanonymity alg onionrouting Installationsanleitung dataleak messenger foodinsecurity password keepassxc partyline cryptography party cybersecuritynews pipewire Installation cryptolalaland solarwinds bitwarden communityalgorithmictrust infosec gchq GemeinsamGegenDieTierindustrie mitm wireless castor repairing IHaveSomethingToHide fotografie passwords gif IronySec cryptowars anonym encryptioncan supplychainattacks UseAMaskUseTor anonymous cyberattack editors security tor comb e2e supplychain bruceschneier gigafactory vpn BlacksInCybersecurity ransomware wireapp toreador itsec dnssecmastery2e openssh factorio Reactorweg openssl backdoored spyware dorfleaks torx encryptionsts e2ee sequoia backdoor cryptotokens NSAmeansNationalScammingAgency stork conscientiousobjectors ed25519 torproject cryptomeanscryptography encryption 0day informationsecurity ssh misshaialert cybersec restore FileSecurity FormFactors crypto theObservatory autokorrektur giftofencryption CyberSecurity foodsecurity kansascity auto signalapp firejail anonymity endtoendcrypto automattic fotografía onionshare onion encryptionist kontor autofahrer infosecbikini autocrypt malware switchtosignal 0days cloudsecurity corydoctorow RestoreOurEarth radiorepair algérie WebAuthn hexeditor nsogroup automotive distortions cryptographyisoverparty opsec InfoSec keepass encryptionists TastySecurity cryptobros securitybyobscurity torsocks toronto nsa autorenleben schneier protonvpn trustissues InsecurityByObscurity yubikey nitrokey encrypted 1password openpgp pgpainless tatort ghibli afraleaks castor9 deletesignal prismbreak gpgtools autodidactic gpg automation fotopiastory equatorial sequoiapgp cybersecurity Tor CryptoWars signal noscript redaktor vector trust backdoors Torge Torfverbrennung sasl emailsecurity cryptoparty pentest wire historia AllmendeKontor itsecurity websecurity foto pgp RobinHoodStore cryptomator signalmessenger openvpn CyberAttack datasecurity autorotate regulators anleitung leak drugstore encryptiost libresignal doctors securitynow storage tracking + + Tue, 10 Aug 2021 08:34:29 UT + + + science + engineering math politicalgeography epidemiology stemfie OpenScienceUN TranslateScience electrochemistry ethnology womeninstem archeology botany STEM biodiversity ocean stemgeenFVD linguistic anthro supercollider nextgeneration zoology linguistics climatology oceans SolarSystems reasoning awk dna geography physics intergenerational archaeologist generalstreik geology ClinicalPsychology generationidentitaire economicanthropology Science SystemicRacism OpenScience corrosion research stemwijzer systemsmap bioengineering GotScience sistemainoperativo stemgeenPVV knowledge stemgeenVVD botanical dream dawkins ineigenersache psychogeography stemgeenVVS holo graphTheory deepdreamgenerator AnnualStatisticalReview trilateralresearch meterology botanicalart JA21 regenerative ScienceDenial biotech stemgeenJA21 regeneration psychology dreamtime pataphysics particles biology bughunting researching_research hunt pacificocean generation gene fossilhunting arthunt badscience mathematics chemistry muon processengineering paleontology oceanography stem anthropocene particlephysics nextgenerationinternet biomedical mechanicalengineering anthropology + + Tue, 10 Aug 2021 08:34:29 UT + + + photos + smartphonephotography nikon 90mm photography fujifilm rewild photogrammetry wildlifephotography wild affinityphoto photocló photo photographe photogrpahy photographer tokyocameraclub nikond90 photos macrophotography photoshop photographie photovoltaik seancephoto camera crops photomanager macropod uwphoto wildbiene macronie photographers cameras fossphotography phototherapie phonephotography myphoto rewilding naturephotography microplastics fediphoto picture wildfood macro intothewild streetphotography FujinonXF90mm wildcat photoreference crop phototherapy pictures + + Tue, 10 Aug 2021 08:34:29 UT + + + history + musichistory heirloom monarchs holocaust history arthistory makeinghistory History anarchisthistory indigenoushistorymonth CarHistory gaminghistory womenshistorymonth NetworkingHistory blackhistory otd monarch computerhistory HistoryOfArt + + Tue, 10 Aug 2021 08:34:29 UT + + + software + beta borgbackup forms app FLOSS freeUP1 freedombox windows edit nginx transclusion krebsrisiken proprietarysoftware cooperativetechnology freepalestin calibre misophonia fosshost postscript nota AAPIHeritageMonth freenet freebsd kc font Framasoft tts E40 Flisol2021 invidious drm freedos softwarelibero alternativesto Raychat publicdomain ilovefreesoftware hydra readers StoryMapJS kubernetes openvms luca nodrm copyleft fossmendations happyauthor freedoom librespeed jami betatesting NottsTV libregraphics genossenschaft FuckOffZoom quicksy thunder whiteboard free docker softwarelibre opensourcehardware uxdesign interoperability impression3d freesoftware gimp krebs backups foss matrix fonts dinosaur mossad unfa weechat clapper designjustice thefreethoughtproject filesystems nextcloud translate wechat notmatrix gnupg lucaApp chats duplicati HappyLight opensourcesoftware permissionless compression ArchLinux openscad freeganizm uidesign softwaredeveloper neochat TabOrder searx ikiwiki prosody Linux FreeSoftware userresearch FlisolLibre2021 DisCOElements Audio rocketchat thanksfreesw libres webapps immers outreachy synapse API freelibre lyft freekirtaner nitter monitoring misogyny virtualbox ngi4eu discord reverseengineering whisperfish ee opensourcedesign vaporware opensource diaspora yunohost oss librelounge AudioCreation chickadee appstore dégooglisons littlebigdetails cabal conferencing cadmium libreboot blueridgeabc musiquelibre mycroft smokefree devops kdeapplications owncast lovewins phabricator emacs freiesoftware FLOSSvol moss fluffychat dinoim impress writefreely videoconferencing bigbluebutton tile_map_editor email moa ngi esri chatapps HappyNewYear Eiskappe fossilfriday umatrix floss plugins softwaresuite frecklesallovertheshow graphic libresoftware softwareengineering mosstodon expandocat deltachat application uifail FOSS peatfree lucaapp GNOMECircle rockpro64 bittorrent palestinewillbefree penpot vlc zoom southasia tiling session diaspora0800 FriendofGNOME Senfstoffknappheit usability winamp opendesign obnam snap appim ProprietarySoftwareProblems pandoc Happy4thJuly freemumia write artificialintelligence blackcappedchickadee cryptpad software libretranslate OwnStream upstream maplibre slack Hummingbard userfreedom hydrated emacslisp Element freeware DismantleFossilCompanies safenetwork asia jit SoftwareLibre zrythm gnu CTZN silicongraphics mumble strugglesessions grsync freecad telegram containers tails freeschool chatons blockchain windows11 irssi HabKeinWhatsapp information mcclim jitsimeet dedrm iso mutt librelingo freetibet WeAreAlmaLinux tilingwm sri design gameoftrees GnuLinuxAudio freegan freeriding freetool backup trueLinuxPhone ngio rotonde freetube jumpdrive GNU speechrecognition eurovison skydroid thunderbird it sound alternativeto screenreader parler bison apps chat licensing fossasia inclusivedesign ethicalsoftware defectivebydesign berne metager digitalsustainability screenreaders ZeroCool LINMOBapps freedombone uber obsproject arti librecast softwareheritage pittsburgh profanity delta Tankklappe doomemacs imageeditor ffmpeg fossandcrafts GNOME40 telesoftware proprietary love notabug reboot opensourcegardens musique switchingsoftware hydrangeas OSM freesw agpl distribute magnifyingglass GNOME freeganizmniewybacza drive botlove duolingo freesoftare AlmaLinux GreenandBlackCross strafmaatschappij freetillie distributedledger mattermost principiadiscordia blue LinuxPhones filesystem rocket ghostscript win10 Zoom tibet ComputerFolklore fossaudio elemental SocialCreditScores flisoltenerife libreops appsec element platforms inclusive uxn librelabucm engineer softwareNotAsAService ptp chatty Matrix lucafail fontawesome informationwantstobefree softwareGripe nativeApp MatrixEffect culturalibre jitsi taintedlove flisol engineers dinosaurier wordpress SwitchToJami mongodb ux rsync libreoffice chatbot crossstitch webdesign Encrochat dino RainbOSM plugin xwiki tecc openoffice container discordia softwaredesign redeslibres ledger sounddesign chatcontrol alternatives glimpse libregraphicsmeeting + + Tue, 10 Aug 2021 08:34:29 UT + + + conferences + FOSDEM2021 stackconf fossnorth debconf debconf21 FOSDEM talk fossdem FreedomBoxSummit apconf2020 schmoocon Aktionscamp realtalk persco penguicon2021 letstalkaboutyes summit confidenceTricks agm libreplanet SeaGL2021 confindustria confluence minidebconf edw2021 maintainerssummit rc3worldleaks rightscon StopStalkerAds SeaGL penguicon emacsconf MCH2021 conferencecalls flossconference LGM2021 conferences LibrePlanet defcon emfcamp flossevent askpinetalk bc conf talks defcon201 rC3 rC3World FOSDEM21 conference mozfest flossconf bootcamp apconf ccc persconferentie GeekBeaconFest rC3one GenCon smalltalk camp g7 C3 config penguicon2022 confy + + Tue, 10 Aug 2021 08:34:29 UT farming johndeere deer - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT facts lifehacking funfact lifehack - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT indymedia - fpga hs2 dotcons visionontv geek tredtionalmedia degeek globleIMC indymediaback openfoodnetwork pga mainstreaming indymedia networking closed stupid foo encryptionsist hs2IMC indymediaIMC network networkmonitoring Blackfoot roadsIMC stupidindivialisam roadstonowhere networkeffect lifecult closedweb avgeek monitor dotconsall omn tv roadstonowhereIMC kiss UKIMC fluffy 4opens openmedianetwork + fpga hs2 dotcons visionontv geek tredtionalmedia indiemedia degeek globleIMC indymediaback pga mainstreaming indymedia closed stupid foo encryptionsist hs2IMC indymediaIMC network networkmonitoring Blackfoot roadsIMC stupidindivialisam roadstonowhere networkeffect lifecult closedweb avgeek monitor dotconsall omn tv roadstonowhereIMC kiss UKIMC fluffy 4opens openmedianetwork - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT cycling - bicycle bicyles cycle bic cycling bicycleday DataRecycling arabic bike motorbike bikeing cyclingtour thingsonbikes openbikesensor bikeways Snowbike cyclist + bicycle bicyles cycle bic cycling bicycleday DataRecycling arabic bike motorbike reusereducerecycle bikeing cyclingtour thingsonbikes openbikesensor bikeways Snowbike cyclist - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT gender - black transparantie transistors transparenz broketrans internationalwomensday2021 transwomen transformativejustice womenwhocode transfobie WomenInHistory sf transmission transgender cashless RaquelvanHaver caféLatte transdayofresistance mens vieillesse womensart blacktranslivesmatter female nonbinary womensday vantascape van blacktransmagic less nb trans nonbinarycommunity transpositivity transdayofvisibility lgbtqia transphobia transmitter women lgbt bodypositive transzorg womenrock estradiol lgbtq transaid queerartist KCHomelessUnion transgenders girlboss pointlesslygendered queer transdayofvisbility genderQuiz gender genderqueerpositivity NonBinaryPositivity dagvandearbeid woman transrights transdayofrevenge + black blackcompany transparantie transistors transparenz broketrans transition internationalwomensday2021 transwomen transformativejustice womenwhocode transfobie buildless WomenInHistory sf transmission transgender cashless RaquelvanHaver caféLatte transdayofresistance mens vieillesse womensart blacktranslivesmatter female nonbinary womensday vantascape van blacktransmagic less nb trans patriarchy nonbinarycommunity transpositivity LucyLawless transdayofvisibility lgbtqia transphobia transmitter women menschheit lgbt bodypositive nonbinarypeoplesday transzorg womenrock estradiol lgbtq transaid queerartist KCHomelessUnion transgenders girlboss pointlesslygendered queer transdayofvisbility nonbinaryday genderQuiz gender genderqueerpositivity NonBinaryPositivity dagvandearbeid woman transrights transdayofrevenge - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT phones - mobileapp cellular fairphone3 téléphone libre nemomobile fairtec linuxfr conocimientolibre manjaro Jingos plasmaDev TourBrandenburg21 rand 5g mobian LinuxPhoneApps lg pine Brandkopf alarmphone androidemulator fdroid plasmamobile shotonpinephone fairuse android smartphonepic nophone ubportsqanda linuxmobile sailfish phones fennecfdroid Mobian osmf AlpineConf smartphone plasma5 ios selinux mobileGNU PinePhoneOrderDay exxon sms4you mob bp microphone linuxconnexion smart smartphones iOS14 pinemarten linuxphones openmoko mobilecoin mobilelinux freeyourandroid fair QWERTYphones exxonmobil sailfishos siskinim epic monal android10 osmocom Smartphones WakeMobile lineageos molly angelfish androiddev Briar manjarolinux quasseldroid wirtschaft plasma mobilephones phosh BriarProject Fairphone librem5 ubportsinstaller osm shotonlibrem5 pinephone Teracube PinePhone pinedio mobile pinephones manjaroarm sms pine64 fairphone ubuntutouch linphone Android osmirl ubports gnomeonmobile immobilienwirtschaft Bramble osmand vodafone gnomemobile linuxonmobile iphones postmarketos iOS microg brandenburg librecellular grapheneos sail recycletechjunkuselinux phone cm mobileKüfA josm iphone linuxappsummit Xperia10mark2 newprofilepic + mobileapp cellular fairphone3 téléphone libre nemomobile fairtec linuxfr conocimientolibre manjaro Jingos plasmaDev TourBrandenburg21 rand 5g mobian LinuxPhoneApps lg pine Brandkopf alarmphone androidemulator fdroid plasmamobile shotonpinephone fairuse android smartphonepic nophone ubportsqanda linuxmobile sailfish phones fennecfdroid Mobian osmf AlpineConf automobile smartphone plasma5 ios selinux mobileGNU PinePhoneOrderDay exxon sms4you mob bp microphone linuxconnexion smart smartphones iOS14 pinemarten linuxphones openmoko mobilecoin mobilelinux freeyourandroid fair QWERTYphones exxonmobil sailfishos siskinim epic monal android10 osmocom Smartphones WakeMobile androids lineageos molly angelfish androiddev Briar manjarolinux quasseldroid wirtschaft plasma mobilephones phosh BriarProject Fairphone librem5 ubportsinstaller linuxphone shotonlibrem5 pinephone Teracube PinePhone pinedio mobile pinephones manjaroarm sms pine64 automobiles fairphone ubuntutouch linphone Android osmirl ubports gnomeonmobile immobilienwirtschaft Bramble osmand vodafone gnomemobile linuxonmobile iphones postmarketos iOS microg brandenburg librecellular GetSession grapheneos sail recycletechjunkuselinux phone cm mobileKüfA lineage josm iphone linuxappsummit Xperia10mark2 newprofilepic - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT activism - UniteInResistance rightwing rights protestor dutysolicitor roots WeDemandTransparency CallToAction annonce rojava PrisonLivesMatter clearchannel nog20 Lobauautobahn farright eni tyrannyofconvenience grassroot nonviolentcommunication FreeLibreOpen g20 JusticeForRapheal rig bekannt farmersprotest animalrights protests resistance cyborgrights riseup resistg7 DontShootTheMessenger demo PrisonSolidarity linnemann sflc DanniVive apt freeassange dangote reuse stopspyingonus keepiton Dannenroederforst FSFE20 fsfe killthebill edri softwarefreedom indigenousrights unautremondeestpossible AntiCopyright Rojava ilovefs stopnacjonalizmowi ann activist wec HeroesResist edrigram xr SustainableUserFreedom bannerlord undercurrents riseup4rojava righttoexist seachange directaction mannheim Doulingo politicalactivism diskriminierung wechange seattleprotests eff Gardening gamechanger change openrightsgroup protest icantbreathe channelname JeffreySDukes planning FSF userrights LaptevSea actiondirecte kroymann climatechange protestsupport channel climatchange HS2 ngo MarcWittmann StandWithTillie Danni FrightfulFive fsf fsfi StopHS2 grassroots HS2Rebellion protestcamp resist FreeJournalistAssange announcements antireport ClimateJustice duolingo RodrigoNunes FreedomCamping BLM ExtinctionRebellion shellmustfall namechange changeisinyourhands wlroots weareallassange conservancy ngos UserFreedom sp bin JefferySaunders freepalestine CopsOffCampus GreatGreenWall LiliannePloumen freeassangenow savetheplanet directactiongetsthegoods hauptmann activismandlaw climatechangeadaptation Kolektiva XR freeolabini tellthetruth announcement isolateByoblu annieleonard + UniteInResistance rightwing rights activism protestor dutysolicitor roots WeDemandTransparency CallToAction annonce rojava PrisonLivesMatter clearchannel nog20 Lobauautobahn farright eni tyrannyofconvenience grassroot nonviolentcommunication FreeLibreOpen protesters g20 ShaleMustFall JusticeForRapheal rig augustriseup bekannt farmersprotest animalrights protests resistance cyborgrights riseup resistg7 DontShootTheMessenger demo PrisonSolidarity linnemann sflc uprootthesystem DanniVive apt freeassange dangote reuse stopspyingonus keepiton Dannenroederforst FSFE20 ClimateJusticeMovement fsfe killthebill edri softwarefreedom indigenousrights activists unautremondeestpossible AntiCopyright Rojava ilovefs stopnacjonalizmowi ann activist wec HeroesResist edrigram xr SustainableUserFreedom bannerlord systemchangenotclimatechange undercurrents riseup4rojava righttoexist seachange directaction mannheim Doulingo politicalactivism diskriminierung wechange seattleprotests eff Gardening gamechanger root change openrightsgroup protest icantbreathe channelname JeffreySDukes planning FSF userrights LaptevSea actiondirecte kroymann climatechange protestsupport channel climatchange HS2 ngo MarcWittmann StandWithTillie Danni FrightfulFive fsf fsfi StopHS2 grassroots HS2Rebellion protestcamp resist openrights TalesFromTheExtinction FreeJournalistAssange announcements antireport ClimateJustice RodrigoNunes FreedomCamping BLM ExtinctionRebellion shellmustfall namechange changeisinyourhands wlroots weareallassange conservancy ngos UserFreedom sp bin JefferySaunders freepalestine CopsOffCampus GreatGreenWall LiliannePloumen freeassangenow savetheplanet freeradical directactiongetsthegoods hauptmann activismandlaw climatechangeadaptation Kolektiva Indigenousresistance BayouBridgePipeline XR freeolabini tellthetruth announcement isolateByoblu annieleonard - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT accessibility you a11y accessibility captionyourimages hardofhearing - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT pandemic - covid19 coronaPolicies gevaccineerd corona getvaccinated CovidImpacts psmeandmywholefamilycaughtcovidfromwork Coronavirus CoronaWarnApp facemasks vaccines wijvaccineren culturalrevolution pandemics vaccine vaccinesupply JournalistsSpeakUpForAssange Covid vaccinated coranavirus pandemic sayhername internationalproletarianrevolution Zbalermorna covidville ZeroCovid pandemia coronapps volkstheater COVID19india contacttracing coronavaccinatie SùghAnEòrna tier4 coronapandemie covid pand volla volodine COVID19NL Moderna coronavirus masks Moderna2 COVIDrelief coronapas virus contacttracingapps moderna coronadebat vaccin COVIDー19 Lockdown rna unvaccinated codid19 CripCOVID19 LongCovid COVID19 vaccination YesWeWork ContactTracing vol CoronaCrisis COVID coronamaatregelen debat international internationalsolidarity coronabeleid + covid19 coronaPolicies gevaccineerd corona getvaccinated CovidImpacts psmeandmywholefamilycaughtcovidfromwork Coronavirus CoronaWarnApp facemasks vaccines wijvaccineren culturalrevolution pandemics vaccine vaccinesupply JournalistsSpeakUpForAssange Covid vaccinated coranavirus NoCovidMonopolies pandemic sayhername internationalproletarianrevolution Zbalermorna internationalcatday covidville ZeroCovid vaccini pandemia coronapps volkstheater COVID19india contacttracing VaccinePatents coronavaccinatie SùghAnEòrna tier4 coronapandemie covid pand SarsCoV2 volla volodine COVID19NL covidmask Moderna coronavirus masks viruses Moderna2 COVIDrelief coronapas virus contacttracingapps moderna coronadebat vaccin COVIDー19 Lockdown rna unvaccinated codid19 CripCOVID19 LongCovid COVID19 vaccination YesWeWork ContactTracing vol coronaviruses CoronaCrisis COVID coronamaatregelen debat international internationalsolidarity coronabeleid - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT books - readinggroup bookstore publicvoit bookbinding preview justhollythings secondhandbooks bookclub fake earthsea review ebooks docbook book notebook public amreading publishing republicday publichealth bookworm bookwyrm 5minsketch republique bookreview reading sketching theLibrary audiobooks Gempub selfpublishing sketchbook wayfarers books peerreview bookreviews failbooks sketch ebook wikibooks booktodon epub cookbook bibliothèque AnarchoBookClub + readinggroup bookstore publicvoit bookbinding preview justhollythings secondhandbooks bookclub fake earthsea review ebooks docbook book notebook public amreading publishing republicday publichealth bookworm bookwyrm 5minsketch artbook republique bookreview reading sketching theLibrary audiobooks Gempub selfpublishing sketchbook wayfarers books peerreview bookreviews failbooks sketch ebook wikibooks booktodon epub cookbook bibliothèque AnarchoBookClub - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT crafts - repair topic_imadethis hackerexchange exchange quilts textile upholstery hackgregator gatos gato hackspacers nrw shack 3dmodeling dust3d hackerspaces hacklab hackerexchange + repair topic_imadethis hackerexchange exchange quilts textile upholstery hackgregator gatos gato hackspacers nrw shack 3dmodeling dust3d hackerspaces hacklab tryhackme sanding solvespace theglassroom sundiy craft wirtschafthacken papercrafts maker knitting hack workspace craftsmanship wood hacked Sipcraft calligraphy biohacking wip spacecrafts hacktheplanet jewelry diy textiles projects hackerweekend handicrafts Handicraft lovecraftcountry upcycling Minecraft woodworking 3dcad glass origami hackerexchange -]] tryhackme sanding solvespace theglassroom sundiy craft wirtschafthacken papercrafts maker knitting hack workspace hacked Sipcraft calligraphy biohacking wip spacecrafts hacktheplanet jewelry diy textiles projects hackerweekend handicrafts Handicraft lovecraftcountry upcycling Minecraft woodworking 3dcad glass origami hackerexchange - - -]] makers nrwe quilting crafting hacker quilt crafts rwe weaving 3dmodel handtools tinkering project hacking woodwork ceramics embroidery shacks teardown +]] makers nrwe quilting crafting sparkwoodand21 hacker quilt crafts rwe weaving 3dmodel handtools tinkering project hacking woodwork ceramics handmade embroidery shacks teardown - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT war - ru DonavynCoffey Myanmarmilitarycoup civilwar antiwar bomber coup tank handforth landmine tankies military autonomousweapons army Etankstelle weaponsofmathdestruction conflict navy warplane fort guns Myanmarcoup weapons siege hbomberguy battle WMD wmd airforce forth + ru DonavynCoffey Myanmarmilitarycoup civilwar antiwar bomber coup weapon tank handforth landmine tankies military autonomousweapons army Etankstelle weaponsofmathdestruction conflict navy warplane fort guns Myanmarcoup weapons siege hbomberguy battle WMD wmd airforce forth - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT techbros bubbles bubble color redbubble securedrop einfachredeneben redditodicittadinanza coloredpencil redhat redwood hackernews weareredhat redmi red pencil reddit redon redis infrared VendrediNouka redshift optreden sec - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT astronomy - telescope immersspace mercury pluto planets galaxy spaceport venus mars bloodmoon amateurastronomy uranus spacex nebula astronomy neptune space jupiter rpc blackhole asteroid BackYardAstronomy moon thehitchhikersguidetothegalaxy observatory euspace asteroidos saturn milkyway spacelarpcafe + telescope immersspace mercury guide pluto planets galaxy spaceport venus mars bloodmoon amateurastronomy uranus spacex nebula astronomy hubblespacetelescope neptune space jupiter rpc blackhole asteroid BackYardAstronomy moon thehitchhikersguidetothegalaxy observatory euspace asteroidos saturn milkyway telescopes spacelarpcafe - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT other - ageassurance bullshit klimaatbeleid justasleepypanda extinctionrebellion fail masseffect lastpass yolo nothingnew Lastpass extinction weareclosed bripe MasseyUniversity PassSanitaire solution messageToSelf TagGegenAntimuslimischenRassismus quecksilber itscomplicated Erzvorkommen test isntreal rzeźwołyńska massextinction misc manutentore frantzfanon shots assaultcube shitpost biomassacentrale mining rising devilsadvocate ACA pinside xp impfpass cda rant Terrassen righttodisassemble rassismus MassoudBarzani koerden CovPass nahrungskette SomeUsefulAndRelvantHashtag LanguageHelpForMigrants nsfw dungeonsAndDragons biomass rassismustötet oversleep ass id Chiacoin futtermittel geo oerde m assassinfly migrantstruggles sleep PointlessGriping close decluttering OCUPACAOCARLOSMARIGHELLA + ageassurance pentester bullshit klimaatbeleid justasleepypanda extinctionrebellion fail masseffect lastpass yolo nothingnew Lastpass extinction weareclosed happy efail bripe MasseyUniversity PassSanitaire solution dansenmetjanssen messageToSelf TagGegenAntimuslimischenRassismus quecksilber itscomplicated Erzvorkommen test isntreal gentests rzeźwołyńska massextinction misc tw rants manutentore frantzfanon shots assaultcube shitpost denachtvanjanssen biomassacentrale mining rising devilsadvocate ACA pinside xp impfpass cda rant Terrassen righttodisassemble rassismus MassoudBarzani koerden CovPass nahrungskette SomeUsefulAndRelvantHashtag LanguageHelpForMigrants nsfw dungeonsAndDragons biomass rassismustötet oversleep ass id Chiacoin futtermittel CubanProtests geo oerde m assassinfly migrantstruggles sleep PointlessGriping close decluttering OCUPACAOCARLOSMARIGHELLA - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT photography - peppercarrotmini NoShothgunParsers pea landscapephotography landscapeart XSystem darktable photograph peppercarrot speakers hippeastrum landscape blackandwhite hot twinpeaks + peppercarrotmini NoShothgunParsers pea CanonSL2 landscapephotography landscapeart XSystem darktable photograph peppercarrot speakers hippeastrum landscape blackandwhite hot twinpeaks - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT month - maythe4thbewithyou yt ots april juneteenth PrideMonth2021 1may july VeganMay march pridemonth chapril marchofrobots2021 october november august june blackherstorymonth december september augustusinc may feburary jejune PrideMonth january marchofrobots blackhistorymonth march4justice month robots maythe4th blacktheirstorymonth + maythe4thbewithyou yt ots april juneteenth PrideMonth2021 bots 1may july VeganMay march pridemonth chapril marchofrobots2021 october november august june blackherstorymonth december september augustusinc may feburary jejune PrideMonth january marchofrobots eternalseptember blackhistorymonth march4justice month robots maythe4th blacktheirstorymonth - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT news - basicincome report news krautreporter flash basic Wikileaks newsletter aljazeera nothingnews newsflash contemporaneous_reports newsroom EUNews Worldnews rt bbc foxnews journalismisnotacrime News bbcbasic goodnews flashcrash doubledownnews bbcnews reuters newschool theguardian badReporting newsboat journalism SkyNews lobsters + basicincome report news 56kNews Newsfeed krautreporter flash basic Wikileaks newsletter aljazeera nothingnews newsflash contemporaneous_reports newsroom EUNews Worldnews rt bbc foxnews journalismisnotacrime News bbcbasic goodnews flashcrash doubledownnews bbcnews reuters newschool theguardian fieldreport badReporting newsboat journalism SkyNews crash lobsters - Tue, 13 Jul 2021 08:43:43 UT - - - health - merchandise FreedomIsTheOnlyTreatment gnuhealth water 4 medical CoronaApp bundesregierung runningdownthewalls autism burnout Underunderstood cannabis hand event healthinsurance medicine anxiety freshwater inflammation run eternalpuberty NHSDataGrab treatment EmotionalFirstAid safeabortion4all maryjane organisierung autistic BlockBrunsbüttel running neurodivergent health motion crunchbang actuallyautistic meds PatientSafety marijuana suicideprevention mentalhealth postmortem H5N8 healthy DarrenChandler autismmeme einzelhandel drugs atm neurodiverse asperger cigarettes hearingimpairment selfcare autismus - - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT cats - Cat dailycatpic dxp DailyCatVid Cats katze kotorico kot ketikoti qualitätskatze CatsOfMastodon Catshuis Leopard SpaceCatsFightFascism CatBellies catbellies LapCats qualitätskatzen katzen + Cat dailycatpic dxp MastoCats DailyCatVid Cats katze kotorico kot ketikoti qualitätskatze CatsOfMastodon Catshuis Leopard SpaceCatsFightFascism CatBellies catbellies LapCats qualitätskatzen katzen - Tue, 13 Jul 2021 08:43:43 UT - - - media - InfiniTime livestreaming ip digitalmedia mustwatch sustainable videobearbeitung transparency polarbears mediathek mianstreaming stream videoconferencias trad maistreaming ime sustainabilty mixxx shortfilm selfsustainable amstrad kawaiipunkstreams mainstream films streaming weAreAllCrazy video streamdeck puns maiabeyrouti videoconference mix mixed sustainability diymedia Fairtrade film streams massmedia stummfilm submedia theatlantic traditionalmedia videos Internetradio mediawatch mainstreamining newsmedia audiovideo videosynthesis filmnoir wikimedia mixedmedia railroads heat documentary streamers artstream vi folktraditions gstreamer tootstream taina ai mediawiki bear realmedia media independentmedia SiberianTimes theintercept - - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT podcasts beautiful podcasting IntergalacticWasabiHour JenaFahrradies podcast rad radiopodcast postmarketOSpodcast TraditionCruelle podcasting20 tilderadio tildes podcasts tildeverse radverkehr smallisbeautiful fertilizers PineTalk radweg tilvids fahrrad tildetown qtile trillbilliespodcast - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT radio - cbradio worldradioday radiokookpunt hamr freieradios varia why radioamateur shoshanazuboff winlink tootlabradio pouetradio schenklradio dx macintosh radioactive amateurradio radiohost radiokapital talkradio localization nwr vantaradio ca radio healthcare listening hamradio FreeAllPoliticalPrisoners variabroadcasts card10 fastapi webradio freeradio radiobroadcasting radiosurvivor Poecileatricapillus apis radioshow local radio3 noshame osh audycja hackerpublicradio kosher radioalhara Phosh audycjaradiowa california road nowlistening radiobroadcast radiostation mastoradio broadcasting radiodread amateurr radiolibre spazradio anonradio Capitaloceno kolektywneradio io + cbradio worldradioday radiokookpunt hamr freieradios varia why radioamateur shoshanazuboff winlink tootlabradio pouetradio schenklradio dx macintosh radioactive ntsradio amateurradio radiohost radiokapital talkradio localization shortwave nwr vantaradio roadsafety ca radio healthcare listening hamradio FreeAllPoliticalPrisoners variabroadcasts card10 fastapi webradio freeradio radiobroadcasting radiosurvivor Poecileatricapillus apis radioshow local cellbroadcast radio3 noshame osh audycja hackerpublicradio kosher radioalhara Phosh audycjaradiowa california road nowlistening radiobroadcast radiostation mastoradio broadcasting radiodread amateurr radiolibre modelrailroad spazradio anonradio Capitaloceno kolektywneradio io - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT pets - buny spinning catpics shepherd leiningen uninstallman ExposureNotifications ats germanshepherd catofmastodon nin TheRabbitHole staatstrojaner deskcat verification eurocrats QuestioningTechnology toocute cataloging cathedrals petpeeve Stelleninserat acidification reEducationCamp mastodogs rats puppets catbehaviour digidog dogecoin Stallman Coolcats petrats governing dogsofmastodon gentrification evening broadcats gattini bunyPosting benjennings kitten fostercats gamification woningnet WegenErdogan jürgenconings cats uninStallman kittens Uninstallman pet dog scotties ageverification Pruning woningnood acat catontour catsofmastodon leninismo podcatcher meow cute mastocat lenin catstodon dogs reimagining catsofparkdale mastocats W3CSpecification mastodog notpixiethecat londoninnercitykitties cat blackcat furry petitie JuliaKitten dogsofmaston JurgenConings training scottie catcontent UserDomestication + buny spinning catpics shepherd leiningen uninstallman ExposureNotifications ats germanshepherd catofmastodon nin TheRabbitHole staatstrojaner deskcat verification eurocrats QuestioningTechnology toocute cataloging cathedrals petpeeve Stelleninserat acidification reEducationCamp mastodogs rats puppets catbehaviour digidog dogecoin Stallman Coolcats petrats governing dogsofmastodon gentrification evening broadcats gattini bunyPosting benjennings kitten fostercats gamification woningnet WegenErdogan jürgenconings cats uninStallman kittens Uninstallman pet dog scotties Pruning woningnood acat catontour catsofmastodon leninismo podcatcher meow cute mastocat lenin catstodon dogs reimagining catsofparkdale mastocats W3CSpecification mastodog notpixiethecat londoninnercitykitties cat blackcat furry petitie JuliaKitten dogsofmaston JurgenConings training scottie catcontent UserDomestication - Tue, 13 Jul 2021 08:43:43 UT - - - games - appdesign gameofshrooms minecraft soloRPG nbsdgames tetris99 gamestop libregaming ageofempires mondragon BiophilicDesign videogame ksp TerraNil productdesign dungeonmaster gogodotjam AudioGame runequest miniatures dragonfall boardgames computergames creature fucknintendo fudgedice angrydesigner gameassets gamestonk videogames FediDesign gameboy puzzle indiegames gamedesign shadowrun spot godotengine adventuregames chess gamejam nintendoswitch mudrunner mud indiegame game 0ad dragon playlog gameart orca sdg lovewood designfail opengameart sign asset gilgamesh ttrpg gamedev freegames guildwars2 creaturedesign bideogames adventuregame TetrisGore gaming gamemaker gameing nintendo roleplayinggames itch unvanquished gamesdonequick Gamesphere devilutionx rpg gamespot tetris dosgaming supertuxkart freegaming DnD socialdesign cyber2077 godot gamestudies tarot cyberpunk2077 gamesforcats FreeNukum spelunkspoil boardgaming supermariomaker2 neopets minetest omake guildwars dice dnd games - - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT years - newyearsresolutions resolutions Year2020 year 1yrago newyear happynewyear 5yrsago yearoftheox newyearseve + newyearsresolutions resolutions Year2020 year 1yrago newyear happynewyear ox 5yrsago yearoftheox newyearseve - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT philosophy - postmeritocracy post minimalism maximalist Allposts maximalism digitalminimalism postprocess philosophy erp stoic spiderposting postfordismo postmodernism minimalist + postmeritocracy post minimalism maximalist Allposts nationalpost maximalism digitalminimalism postprocess philosophy erp stoic spiderposting postfordismo postmodernism minimalist - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT transport - deutschland luggage Gütertransporte publictransport busses transportation train transport deutsch deutscheumwelthilfe airway journey motorway hilfe deutschebahn travel ev prorail airport rail + deutschland luggage Gütertransporte publictransport busses activetransport transportation train transport trains deutsch deutscheumwelthilfe airway journey motorway aviation deutschebahn travel ev prorail airport rail - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT ethics - licenses digitalethics ethicaltech ethics ethicallicense ethicswashing license ethical ethicsintech + licenses digitalethics ethicaltech ethics ethicallicense ethicswashing ethical ethicsintech - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT commons - ed mentalillness opennmsgroup OpenAccessButton niemandistvergessen distraction open linkedopenactors openaccess reopening openocd openengiadina opennms ess badges opensocial commonscloud activisim openlibrary characters opensourcing innovation openpublishing InstantMessenger LessIsMore openrefine openworlds extraction openwashing publicinterest besserorganisieren exittocommunity openinnovation opennmt openbadges act accessable ManufacturaIndependente openspades Accessibility keinvergessen GetSession openrepos2021 openftw Bessa + ed openformat mentalillness opennmsgroup OpenAccessButton niemandistvergessen distraction open linkedopenactors openaccess reopening openocd openengiadina opennms ess badges opensocial commonscloud activisim openlibrary characters opensourcing innovation openpublishing verge InstantMessenger LessIsMore openrefine openworlds extraction openwashing publicinterest besserorganisieren exittocommunity openinnovation opennmt openbadges act accessable openfest2021 ManufacturaIndependente openspades Accessibility keinvergessen openrepos2021 openftw Bessa - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT seasons - mailspring spring lupin thespinoff Dadvice autumn abolishice desummersummit licenziamenti namedropping office hooping sipping es fuckice winter EthicalLicenses ice luejenspringer hpintegrity pingpong santa summer iced LibreOffice summerschool onlyoffice pinball icedipping solstice unicef wintersolstice FederalOffice summerRolls pin mice + mailspring officehours spring lupin thespinoff Dadvice autumn abolishice desummersummit licenziamenti namedropping office hooping sipping es fuckice winter EthicalLicenses ice luejenspringer hpintegrity pingpong santa summer iced LibreOffice summerschool onlyoffice pinball icedipping solstice unicef officework wintersolstice FederalOffice summerRolls pin mice - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT questions - checking kayaking askmastodon flockingbird biking questions king euskadi asking lockpicking factchecking askfedi basketball askafriend flask GlobalBanOnFracking TraditionalWoodworking question ska askmasto breaking scrap_booking maskengate criticalthinking askfediverse fucking totallyaskingforafriend ask daretoask askfosstodon + checking kayaking askmastodon flockingbird biking questions king euskadi asking mask lockpicking Hacking GlobalFrackingBan factchecking askfedi basketball smoking WorldAgainstFracking askafriend flask GlobalBanOnFracking TraditionalWoodworking question ska askmasto breaking scrap_booking maskengate criticalthinking askfediverse fucking totallyaskingforafriend ask daretoask askfosstodon - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT fiction ABoringDystopia interactivefiction cyberpunk VersGNRWstoppen thehobbit fiction microfiction stopCGL nonfiction DystopianCyberpunkFuture stoptmx top flashfiction cyberpunk2020 genrefiction - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT audio - feed audiophile liveaudio audioproduction feeds pulseaudio audi webaudio feedbackd audioprogramming audioengineering audience audiogames audiofeedback audio auditoriasocial + feed audiophile liveaudio audioproduction feeds pulseaudio audi webaudio feedbackd audioprogramming mastoaudio audioengineering audience audiogames audiofeedback audio auditoriasocial - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT garbage - Anonymous cumbria documentation no QAnonAnonymous docu cardano cum u ChanCulture + Anonymous cumbria documentation no QAnonAnonymous docu cardano documents cum u ChanCulture - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT birds - RainbowBeeEater aves birb pigeon cawbird pigeonlover bird birdwatch birdsite birding birbposting birdwatching + RainbowBeeEater aves birb pigeon cawbird pigeonlover bird birdposting birdwatch birdsite birding birbposting birdwatching - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT disability ableism disabled ableismus - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT travel tax travellers taxi airtravel - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT religion - atheist buddhist ama neopagan pagan catholic paganism genesis jesuit SiddarthaGautama oorlogspropaganda + atheist buddhist ama neopagan pagan catholic paganism genesis jesuit secularism SiddarthaGautama oorlogspropaganda - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT culture etiquette - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT funding - donate disabilitycrowdfund disabledcrowdfund erschöpfung now oled alledoerferbleiben LeylaKhaled ethicalfunding mastercard netzfundstück didyouknow fundraiser BreakWalledGardens ki membership fundamentals nzSuperFund ngizero fun oer zeroknowledge edge led zerohedge DefundLine3 vkickstarter fungiverse alledörferbleiben fungus SmallPiecesLooselyCoupled fungi EntangledLife desperate opencollective patreon FundOSS + donate disabilitycrowdfund disabledcrowdfund erschöpfung funding now oled LuisaToledo alledoerferbleiben LeylaKhaled ethicalfunding mastercard netzfundstück didyouknow fundraiser BreakWalledGardens ki membership fundamentals nzSuperFund ngizero fun oer zeroknowledge edge led zerohedge DefundLine3 vkickstarter fungiverse alledörferbleiben fungus fundingmatters SmallPiecesLooselyCoupled hedgedog fungi EntangledLife desperate opencollective patreon FundOSS - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT identity genx boomer genz zoomer - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT ai macos machinelearning openai EthicsInAI - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT licenses - commongoods creativecommonsrocks voice agplv3 tootle commonvoice CommunitySource place copyright commonspoly creative netcommons common gpl plugplugplug copyrightlaw commonplacebook EthicalSource questioncopyright tragedyofthecommons cc0 creativecommons commongood cc creativetoot + commongoods creativecommonsrocks voice violation agplv3 tootle commoning commonvoice CommunitySource place copyright commonspoly creative netcommons common gpl plugplugplug copyrightlaw commonplacebook license EthicalSource questioncopyright tragedyofthecommons cc0 creativecommons commongood cc creativetoot - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT political - copservation housekeeping gan ram progress slaughterhouse rog cops houseless brogue joerogan theteahouse bibliogram house hydrogen straming theGreenhouse teahouse progressivehouse techhouse clubhouse yayagram PDXdefendthehouseless pdxhouseless EnergyFlowDiagrams pr progress_note deephouse roguelike linguisticProgramming gancio + copservation housekeeping gan ram progress slaughterhouse rog cops houseless brogue progresso joerogan theteahouse bibliogram house hydrogen straming theGreenhouse spycops teahouse progressivehouse techhouse clubhouse yayagram PDXdefendthehouseless pdxhouseless EnergyFlowDiagrams pr progress_note deephouse roguelike linguisticProgramming gancio - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT organisations foundation scpfoundation scp - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT fashion brasil fashionistas fashionesta bras fashionista fashion punkwear earrings socks patches feditats zebras - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT welfare CreditReporting universalcredit welfare socialwelfare credit - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT scotland - lan atlanta northumberland glasgow highlands edinburgh loch + lan atlanta glasgow highlands edinburgh loch - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT antisocial stalking cyberstalking - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT comedy laugh farce humour swisshumor satire irony standup funny humor punishment pun - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT obituaries - ueberwachung siberia tripadvisor rip gretathunberg JavaScriptSucks ratgeber obit ecmascript keyenberg raspberripi CyberSecurity döppersberg cybergrooming Gudensberg überblick obituaries ber civilliberties rubber + ueberwachung siberia tripadvisor rip JavaScriptSucks ratgeber obit ecmascript keyenberg raspberripi döppersberg cybergrooming Gudensberg überblick obituaries ber civilliberties rubber cyber - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT introductions reintroductions newhere firsttoot recommends stt Introduction Introductions reintroduction introductons introduction intro introductions - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT geography - theCartographer + theCartographer graph - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT education - SchoolForAfrica PhDstudent mitbewohnerin techlearningcollective oh languages student teaching tutorials education academics mit academia teach Lebensmittelfarbstoff elearning learning languagelearning tutorial mitkatzundkegel ec language deeplearning collect cad mitteleuropa + SchoolForAfrica PhDstudent mitbewohnerin techlearningcollective oh languages student teaching tutorials education academics mit academia teach Lebensmittelfarbstoff elearning learning languagelearning tutorial mitkatzundkegel ec language deeplearning collect teacher cad mitteleuropa - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT scifi - startrekdiscovery startrek discover SoftwareJob LegDichNieMitSchwarzenKatzenAn starwars ds9 discovery SchwarzeFrauen babylon NGIForward war babylon5 + startrekdiscovery startrek discover SoftwareJob LegDichNieMitSchwarzenKatzenAn starwars ds9 discovery trek SchwarzeFrauen babylon NGIForward war babylon5 - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT microcontroller e kontrollieren microcontroller trolls Chatkontrolle troll arduinoide arduino - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT design userfriendly friendly rf - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT help - mastohelp helpwanted lpf helpful helpMeOutHere help + mastohelp MutualAidRequest helpwanted lpf helpful MutualAidReques hilfe helpMeOutHere help - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT automotive volkswagen - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT fantasy discworld godzilla - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT entertainment - CircusInPlace legallyblonde watching Thundercat makingof entertainment me un nowwatching mandalorian themandalorian nt + CircusInPlace legallyblonde watching theCinema Thundercat makingof entertainment me un nowwatching mandalorian themandalorian nt - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT election Rainbowvote voted vote - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT moderation fedblock - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT languages lojban gaelic - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT environment s crisisclimatica clim climatechaos climateadaptation - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT organization conceptmap mindmapping mapping mindmap notetoself pi - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT + + + industrial + powerplants + + Tue, 10 Aug 2021 08:34:29 UT technology AvatarResearch tools LowtechSolutions literatools - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT microcontrollers esp32c3 microcontrollers esp8266 esp32 - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT agriculture farmers - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT organisation InstitutionalMemory - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT skills gardening baking - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT france Macronavirus - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT memes tired - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT sailing theBoatyard - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT parenting dadposting - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT jewelry bracelet - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT architecture concrete - Tue, 13 Jul 2021 08:43:43 UT + Tue, 10 Aug 2021 08:34:29 UT + + + licences + TVRights + + Tue, 10 Aug 2021 08:34:29 UT diff --git a/delete.py b/delete.py index 834e8c46e..ef042d8a3 100644 --- a/delete.py +++ b/delete.py @@ -18,6 +18,7 @@ from utils import getDomainFromActor from utils import locatePost from utils import deletePost from utils import removeModerationPostFromIndex +from utils import localActorUrl from session import postJson from webfinger import webfingerHandle from auth import createBasicAuthHeader @@ -38,8 +39,7 @@ def sendDeleteViaServer(baseDir: str, session, fromDomainFull = getFullDomain(fromDomain, fromPort) - actor = httpPrefix + '://' + fromDomainFull + \ - '/users/' + fromNickname + actor = localActorUrl(httpPrefix, fromNickname, fromDomainFull) toUrl = 'https://www.w3.org/ns/activitystreams#Public' ccUrl = actor + '/followers' @@ -57,7 +57,7 @@ def sendDeleteViaServer(baseDir: str, session, # lookup the inbox for the To handle wfRequest = \ webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - fromDomain, projectVersion, debug) + fromDomain, projectVersion, debug, False) if not wfRequest: if debug: print('DEBUG: delete webfinger failed for ' + handle) diff --git a/desktop_client.py b/desktop_client.py index 0e2b0bd85..353a682c1 100644 --- a/desktop_client.py +++ b/desktop_client.py @@ -16,6 +16,7 @@ import webbrowser import urllib.parse from pathlib import Path from random import randint +from utils import getBaseContentFromPost from utils import hasObjectDict from utils import getFullDomain from utils import isDM @@ -24,6 +25,7 @@ from utils import removeHtml from utils import getNicknameFromActor from utils import getDomainFromActor from utils import isPGPEncrypted +from utils import localActorUrl from session import createSession from speaker import speakableText from speaker import getSpeakerPitch @@ -415,7 +417,8 @@ def _desktopReplyToPost(session, postId: str, cachedWebfingers: {}, personCache: {}, debug: bool, subject: str, screenreader: str, systemLanguage: str, - espeak) -> None: + espeak, conversationId: str, + lowBandwidth: bool) -> None: """Use the desktop client to send a reply to the most recent post """ if '://' not in postId: @@ -468,7 +471,9 @@ def _desktopReplyToPost(session, postId: str, commentsEnabled, attach, mediaType, attachedImageDescription, city, cachedWebfingers, personCache, isArticle, - debug, postId, postId, subject) == 0: + systemLanguage, lowBandwidth, + debug, postId, postId, + conversationId, subject) == 0: sayStr = 'Reply sent' else: sayStr = 'Reply failed' @@ -481,9 +486,10 @@ def _desktopNewPost(session, cachedWebfingers: {}, personCache: {}, debug: bool, screenreader: str, systemLanguage: str, - espeak) -> None: + espeak, lowBandwidth: bool) -> None: """Use the desktop client to create a new post """ + conversationId = None sayStr = 'Create new post' _sayCommand(sayStr, sayStr, screenreader, systemLanguage, espeak) sayStr = 'Type your post, then press Enter.' @@ -529,7 +535,9 @@ def _desktopNewPost(session, commentsEnabled, attach, mediaType, attachedImageDescription, city, cachedWebfingers, personCache, isArticle, - debug, None, None, subject) == 0: + systemLanguage, lowBandwidth, + debug, None, None, + conversationId, subject) == 0: sayStr = 'Post sent' else: sayStr = 'Post failed' @@ -652,7 +660,8 @@ def _readLocalBoxPost(session, nickname: str, domain: str, pageNumber: int, index: int, boxJson: {}, systemLanguage: str, screenreader: str, espeak, - translate: {}, yourActor: str) -> {}: + translate: {}, yourActor: str, + domainFull: str, personCache: {}) -> {}: """Reads a post from the given timeline Returns the post json """ @@ -687,15 +696,17 @@ def _readLocalBoxPost(session, nickname: str, domain: str, __version__, translate, YTReplacementDomain, allowLocalNetworkAccess, - recentPostsCache, False) + recentPostsCache, False, + systemLanguage, + domainFull, personCache) if postJsonObject2: if hasObjectDict(postJsonObject2): if postJsonObject2['object'].get('attributedTo') and \ postJsonObject2['object'].get('content'): attributedTo = postJsonObject2['object']['attributedTo'] - content = postJsonObject2['object']['content'] - if isinstance(attributedTo, str) and \ - isinstance(content, str): + content = \ + getBaseContentFromPost(postJsonObject2, systemLanguage) + if isinstance(attributedTo, str) and content: actor = attributedTo nameStr += ' ' + translate['announces'] + ' ' + \ getNicknameFromActor(actor) @@ -719,7 +730,7 @@ def _readLocalBoxPost(session, nickname: str, domain: str, attributedTo = postJsonObject['object']['attributedTo'] if not attributedTo: return {} - content = postJsonObject['object']['content'] + content = getBaseContentFromPost(postJsonObject, systemLanguage) if not isinstance(attributedTo, str) or \ not isinstance(content, str): return {} @@ -1042,7 +1053,8 @@ def _desktopShowBox(indent: str, published = _formatPublished(postJsonObject['published']) - content = _textOnlyContent(postJsonObject['object']['content']) + contentStr = getBaseContentFromPost(postJsonObject, systemLanguage) + content = _textOnlyContent(contentStr) if boxName != 'dm': if isDM(postJsonObject): content = '📧' + content @@ -1100,7 +1112,7 @@ def _desktopNewDM(session, toHandle: str, cachedWebfingers: {}, personCache: {}, debug: bool, screenreader: str, systemLanguage: str, - espeak) -> None: + espeak, lowBandwidth: bool) -> None: """Use the desktop client to create a new direct message which can include multiple destination handles """ @@ -1121,7 +1133,7 @@ def _desktopNewDM(session, toHandle: str, cachedWebfingers, personCache, debug, screenreader, systemLanguage, - espeak) + espeak, lowBandwidth) def _desktopNewDMbase(session, toHandle: str, @@ -1130,9 +1142,10 @@ def _desktopNewDMbase(session, toHandle: str, cachedWebfingers: {}, personCache: {}, debug: bool, screenreader: str, systemLanguage: str, - espeak) -> None: + espeak, lowBandwidth: bool) -> None: """Use the desktop client to create a new direct message """ + conversationId = None toPort = port if '://' in toHandle: toNickname = getNicknameFromActor(toHandle) @@ -1217,7 +1230,9 @@ def _desktopNewDMbase(session, toHandle: str, commentsEnabled, attach, mediaType, attachedImageDescription, city, cachedWebfingers, personCache, isArticle, - debug, None, None, subject) == 0: + systemLanguage, lowBandwidth, + debug, None, None, + conversationId, subject) == 0: sayStr = 'Direct message sent' else: sayStr = 'Direct message failed' @@ -1282,7 +1297,7 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, storeInboxPosts: bool, showNewPosts: bool, language: str, - debug: bool) -> None: + debug: bool, lowBandwidth: bool) -> None: """Runs the desktop and screen reader client, which announces new inbox items """ @@ -1360,7 +1375,7 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, systemLanguage, espeak) domainFull = getFullDomain(domain, port) - yourActor = httpPrefix + '://' + domainFull + '/users/' + nickname + yourActor = localActorUrl(httpPrefix, nickname, domainFull) actorJson = None notifyJson = { @@ -1590,7 +1605,8 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, httpPrefix, baseDir, currTimeline, pageNumber, postIndex, boxJson, systemLanguage, screenreader, - espeak, translate, yourActor) + espeak, translate, yourActor, + domainFull, personCache) print('') sayStr = 'Press Enter to continue...' sayStr2 = _highlightText(sayStr) @@ -1661,6 +1677,10 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, subject = None if postJsonObject['object'].get('summary'): subject = postJsonObject['object']['summary'] + conversationId = None + if postJsonObject['object'].get('conversation'): + conversationId = \ + postJsonObject['object']['conversation'] sessionReply = createSession(proxyType) _desktopReplyToPost(sessionReply, postId, baseDir, nickname, password, @@ -1668,7 +1688,8 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, cachedWebfingers, personCache, debug, subject, screenreader, systemLanguage, - espeak) + espeak, conversationId, + lowBandwidth) refreshTimeline = True print('') elif (commandStr == 'post' or commandStr == 'p' or @@ -1702,7 +1723,7 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, cachedWebfingers, personCache, debug, screenreader, systemLanguage, - espeak) + espeak, lowBandwidth) refreshTimeline = True else: # public post @@ -1712,7 +1733,7 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, cachedWebfingers, personCache, debug, screenreader, systemLanguage, - espeak) + espeak, lowBandwidth) refreshTimeline = True print('') elif commandStr == 'like' or commandStr.startswith('like '): @@ -1929,8 +1950,8 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, blockDomain = blockHandle.split('@')[1] blockNickname = blockHandle.split('@')[0] blockActor = \ - httpPrefix + '://' + blockDomain + \ - '/users/' + blockNickname + localActorUrl(httpPrefix, + blockNickname, blockDomain) if currIndex > 0 and boxJson and not blockActor: postJsonObject = \ _desktopGetBoxPostObject(boxJson, currIndex) @@ -2318,11 +2339,14 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, __version__, translate, YTReplacementDomain, allowLocalNetworkAccess, - recentPostsCache, False) + recentPostsCache, False, + systemLanguage, + domainFull, personCache) if postJsonObject2: postJsonObject = postJsonObject2 if postJsonObject: - content = postJsonObject['object']['content'] + content = \ + getBaseContentFromPost(postJsonObject, systemLanguage) messageStr, detectedLinks = \ speakableText(baseDir, content, translate) linkOpened = False @@ -2378,7 +2402,9 @@ def runDesktopClient(baseDir: str, proxyType: str, httpPrefix: str, print('') if postJsonObject['object'].get('summary'): print(postJsonObject['object']['summary']) - print(postJsonObject['object']['content']) + contentStr = getBaseContentFromPost(postJsonObject, + systemLanguage) + print(contentStr) print('') sayStr = 'Confirm delete, yes or no?' _sayCommand(sayStr, sayStr, screenreader, diff --git a/devices.py b/devices.py index b2fb09707..8b56e5d37 100644 --- a/devices.py +++ b/devices.py @@ -34,6 +34,7 @@ import os from utils import loadJson from utils import saveJson from utils import acctDir +from utils import localActorUrl def E2EEremoveDevice(baseDir: str, nickname: str, domain: str, @@ -142,7 +143,7 @@ def E2EEdevicesCollection(baseDir: str, nickname: str, domain: str, personDir = acctDir(baseDir, nickname, domain) if not os.path.isdir(personDir): return {} - personId = httpPrefix + '://' + domainFull + '/users/' + nickname + personId = localActorUrl(httpPrefix, nickname, domainFull) if not os.path.isdir(personDir + '/devices'): os.mkdir(personDir + '/devices') deviceList = [] diff --git a/donate.py b/donate.py index f0942e502..a8f4e26d3 100644 --- a/donate.py +++ b/donate.py @@ -8,12 +8,16 @@ __status__ = "Production" __module_group__ = "Profile Metadata" -def _getDonationTypes() -> str: +def _getDonationTypes() -> []: return ('patreon', 'paypal', 'gofundme', 'liberapay', 'kickstarter', 'indiegogo', 'crowdsupply', 'subscribestar') +def _getWebsiteStrings() -> []: + return ['www', 'website', 'web', 'homepage'] + + def getDonationUrl(actorJson: {}) -> str: """Returns a link used for donations """ @@ -39,6 +43,28 @@ def getDonationUrl(actorJson: {}) -> str: return '' +def getWebsite(actorJson: {}, translate: {}) -> str: + """Returns a web address link + """ + if not actorJson.get('attachment'): + return '' + matchStrings = _getWebsiteStrings() + matchStrings.append(translate['Website'].lower()) + for propertyValue in actorJson['attachment']: + if not propertyValue.get('name'): + continue + if propertyValue['name'].lower() not in matchStrings: + continue + if not propertyValue.get('type'): + continue + if not propertyValue.get('value'): + continue + if propertyValue['type'] != 'PropertyValue': + continue + return propertyValue['value'] + return '' + + def setDonationUrl(actorJson: {}, donateUrl: str) -> None: """Sets a link used for donations """ @@ -102,3 +128,47 @@ def setDonationUrl(actorJson: {}, donateUrl: str) -> None: "value": donateValue } actorJson['attachment'].append(newDonate) + + +def setWebsite(actorJson: {}, websiteUrl: str, translate: {}) -> None: + """Sets a web address + """ + websiteUrl = websiteUrl.strip() + notUrl = False + if '.' not in websiteUrl: + notUrl = True + if '://' not in websiteUrl: + notUrl = True + if ' ' in websiteUrl: + notUrl = True + if '<' in websiteUrl: + notUrl = True + + if not actorJson.get('attachment'): + actorJson['attachment'] = [] + + matchStrings = _getWebsiteStrings() + matchStrings.append(translate['Website'].lower()) + + # remove any existing value + propertyFound = None + for propertyValue in actorJson['attachment']: + if not propertyValue.get('name'): + continue + if not propertyValue.get('type'): + continue + if propertyValue['name'].lower() not in matchStrings: + continue + propertyFound = propertyValue + break + if propertyFound: + actorJson['attachment'].remove(propertyFound) + if notUrl: + return + + newEntry = { + "name": 'Website', + "type": "PropertyValue", + "value": websiteUrl + } + actorJson['attachment'].append(newEntry) diff --git a/emoji/blobthinksmart.png b/emoji/blobthinksmart.png new file mode 100644 index 000000000..59dd64f73 Binary files /dev/null and b/emoji/blobthinksmart.png differ diff --git a/emoji/default_emoji.json b/emoji/default_emoji.json index a7170135f..ab8ac89f3 100644 --- a/emoji/default_emoji.json +++ b/emoji/default_emoji.json @@ -519,6 +519,7 @@ "zorin": "zorin", "solus": "solus", "fedora": "fedora", + "redhat": "redhat", "elementary": "elementary", "prideflag": "pride", "biflag": "biflag", @@ -671,6 +672,7 @@ "meownwn": "meownwn", "meowderpy": "meowderpy", "blobcat": "blobcat", + "blobthinksmart": "blobthinksmart", "blobcathappy": "blobcathappy", "blobcoolcat": "blobcoolcat", "blobcatwink": "blobcatwink", @@ -766,5 +768,6 @@ "pine64": "pine64", "void": "void", "openbsd": "openbsd", - "freebsd": "freebsd" + "freebsd": "freebsd", + "orgmode": "orgmode" } diff --git a/emoji/orgmode.png b/emoji/orgmode.png new file mode 100644 index 000000000..e3e35866e Binary files /dev/null and b/emoji/orgmode.png differ diff --git a/emoji/redhat.png b/emoji/redhat.png new file mode 100644 index 000000000..968fbfbb9 Binary files /dev/null and b/emoji/redhat.png differ diff --git a/epicyon-profile.css b/epicyon-profile.css index e1cc91c4d..8a91c7ddf 100644 --- a/epicyon-profile.css +++ b/epicyon-profile.css @@ -136,6 +136,8 @@ --containericons-horizontal-spacing: 1%; --containericons-horizontal-spacing-mobile: 3%; --containericons-horizontal-offset: -1%; + --containericons-vertical-align: 0.5%; + --containericons-vertical-align-mobile: 1%; --likes-count-offset: 5px; --likes-count-offset-mobile: 10px; --publish-button-vertical-offset: 10px; @@ -1308,8 +1310,8 @@ div.container { .containericons img { float: var(--icons-side); max-width: 200px; - width: 3%; - margin: 0px var(--containericons-horizontal-spacing); + width: 25px; + margin: var(--containericons-vertical-align) var(--containericons-horizontal-spacing); margin-right: 0px; border-radius: 0%; } @@ -1512,7 +1514,7 @@ div.container { color: var(--time-color); margin: var(--time-vertical-align) 20px; } - input[type=text], select, textarea { + input[type=text], input[type=password], select, textarea { width: 100%; padding: 12px; border: 1px solid #ccc; @@ -1970,7 +1972,7 @@ div.container { float: var(--icons-side); max-width: 200px; width: 7%; - margin: 1% var(--containericons-horizontal-spacing-mobile); + margin: var(--containericons-vertical-align-mobile) var(--containericons-horizontal-spacing-mobile); margin-right: 0px; border-radius: 0%; } @@ -2166,7 +2168,7 @@ div.container { color: var(--time-color); margin: var(--time-vertical-align-mobile) 20px; } - input[type=text], select, textarea { + input[type=text], input[type=password], select, textarea { width: 100%; padding: 12px; border: 1px solid #ccc; diff --git a/epicyon.py b/epicyon.py index 0fc7f86d6..6748ee1b4 100644 --- a/epicyon.py +++ b/epicyon.py @@ -54,6 +54,8 @@ from follow import clearFollows from follow import followerOfPerson from follow import sendFollowRequestViaServer from follow import sendUnfollowRequestViaServer +from tests import testSharedItemsFederation +from tests import testGroupFollow from tests import testPostMessageBetweenServers from tests import testFollowBetweenServers from tests import testClientToServer @@ -85,6 +87,8 @@ from manualapprove import manualDenyFollowRequest from manualapprove import manualApproveFollowRequest from shares import sendShareViaServer from shares import sendUndoShareViaServer +from shares import sendWantedViaServer +from shares import sendUndoWantedViaServer from shares import addShare from theme import setTheme from announce import sendAnnounceViaServer @@ -110,6 +114,20 @@ parser = argparse.ArgumentParser(description='ActivityPub Server') parser.add_argument('--userAgentBlocks', type=str, default=None, help='List of blocked user agents, separated by commas') +parser.add_argument('--libretranslate', dest='libretranslateUrl', type=str, + default=None, + help='URL for LibreTranslate service') +parser.add_argument('--conversationId', dest='conversationId', type=str, + default=None, + help='Conversation Id which can be added ' + + 'when sending a post') +parser.add_argument('--libretranslateApiKey', + dest='libretranslateApiKey', type=str, + default=None, + help='API key for LibreTranslate service') +parser.add_argument('--defaultCurrency', dest='defaultCurrency', type=str, + default=None, + help='Default currency EUR/GBP/USD...') parser.add_argument('-n', '--nickname', dest='nickname', type=str, default=None, help='Nickname of the account to use') @@ -257,6 +275,10 @@ parser.add_argument('--rss', dest='rss', type=str, default=None, help='Show an rss feed for a given url') parser.add_argument('-f', '--federate', nargs='+', dest='federationList', help='Specify federation list separated by spaces') +parser.add_argument('--federateshares', nargs='+', + dest='sharedItemsFederatedDomains', + help='Specify federation list for shared items, ' + + 'separated by spaces') parser.add_argument("--following", "--followingList", dest='followingList', type=str2bool, nargs='?', @@ -309,6 +331,11 @@ parser.add_argument("--rssIconAtTop", const=True, default=True, help="Whether to show the rss icon at teh top or bottom" + "of the timeline") +parser.add_argument("--lowBandwidth", + dest='lowBandwidth', + type=str2bool, nargs='?', + const=True, default=True, + help="Whether to use low bandwidth images") parser.add_argument("--publishButtonAtTop", dest='publishButtonAtTop', type=str2bool, nargs='?', @@ -437,6 +464,9 @@ parser.add_argument('--minimumvotes', dest='minimumvotes', type=int, default=1, help='Minimum number of votes to remove or add' + ' a newswire item') +parser.add_argument('--maxLikeCount', dest='maxLikeCount', type=int, + default=10, + help='Maximum number of likes displayed on a post') parser.add_argument('--votingtime', dest='votingtime', type=int, default=1440, help='Time to vote on newswire items in minutes') @@ -545,12 +575,27 @@ parser.add_argument('--itemName', dest='itemName', type=str, parser.add_argument('--undoItemName', dest='undoItemName', type=str, default=None, help='Name of an shared item to remove') +parser.add_argument('--wantedItemName', dest='wantedItemName', type=str, + default=None, + help='Name of a wanted item') +parser.add_argument('--undoWantedItemName', dest='undoWantedItemName', + type=str, default=None, + help='Name of a wanted item to remove') parser.add_argument('--summary', dest='summary', type=str, default=None, help='Description of an item being shared') parser.add_argument('--itemImage', dest='itemImage', type=str, default=None, help='Filename of an image for an item being shared') +parser.add_argument('--itemQty', dest='itemQty', type=float, + default=1, + help='Quantity of items being shared') +parser.add_argument('--itemPrice', dest='itemPrice', type=str, + default="0.00", + help='Total price of items being shared') +parser.add_argument('--itemCurrency', dest='itemCurrency', type=str, + default="EUR", + help='Currency of items being shared') parser.add_argument('--itemType', dest='itemType', type=str, default=None, help='Type of item being shared') @@ -588,6 +633,8 @@ if args.tests: sys.exit() if args.testsnetwork: print('Network Tests') + testSharedItemsFederation() + testGroupFollow() testPostMessageBetweenServers() testFollowBetweenServers() testClientToServer() @@ -606,6 +653,14 @@ if baseDir.endswith('/'): print("--path option should not end with '/'") sys.exit() +# automatic translations +if args.libretranslateUrl: + if '://' in args.libretranslateUrl and \ + '.' in args.libretranslateUrl: + setConfigParam(baseDir, 'libretranslateUrl', args.libretranslateUrl) +if args.libretranslateApiKey: + setConfigParam(baseDir, 'libretranslateApiKey', args.libretranslateApiKey) + if args.posts: if '@' not in args.posts: if '/users/' in args.posts: @@ -631,9 +686,11 @@ if args.posts: args.port = 80 elif args.gnunet: proxyType = 'gnunet' + if not args.language: + args.language = 'en' getPublicPostsOfPerson(baseDir, nickname, domain, False, True, proxyType, args.port, httpPrefix, debug, - __version__) + __version__, args.language) sys.exit() if args.postDomains: @@ -663,12 +720,15 @@ if args.postDomains: proxyType = 'gnunet' wordFrequency = {} domainList = [] + if not args.language: + args.language = 'en' domainList = getPublicPostDomains(None, baseDir, nickname, domain, proxyType, args.port, httpPrefix, debug, __version__, - wordFrequency, domainList) + wordFrequency, domainList, + args.language) for postDomain in domainList: print(postDomain) sys.exit() @@ -703,12 +763,15 @@ if args.postDomainsBlocked: proxyType = 'gnunet' wordFrequency = {} domainList = [] + if not args.language: + args.language = 'en' domainList = getPublicPostDomainsBlocked(None, baseDir, nickname, domain, proxyType, args.port, httpPrefix, debug, __version__, - wordFrequency, domainList) + wordFrequency, domainList, + args.language) for postDomain in domainList: print(postDomain) sys.exit() @@ -741,12 +804,14 @@ if args.checkDomains: elif args.gnunet: proxyType = 'gnunet' maxBlockedDomains = 0 + if not args.language: + args.language = 'en' checkDomains(None, baseDir, nickname, domain, proxyType, args.port, httpPrefix, debug, __version__, - maxBlockedDomains, False) + maxBlockedDomains, False, args.language) sys.exit() if args.socnet: @@ -758,10 +823,12 @@ if args.socnet: if not args.http: args.port = 443 proxyType = 'tor' + if not args.language: + args.language = 'en' dotGraph = instancesGraph(baseDir, args.socnet, proxyType, args.port, httpPrefix, debug, - __version__) + __version__, args.language) try: with open('socnet.dot', 'w+') as fp: fp.write(dotGraph) @@ -785,9 +852,11 @@ if args.postsraw: proxyType = 'i2p' elif args.gnunet: proxyType = 'gnunet' + if not args.language: + args.language = 'en' getPublicPostsOfPerson(baseDir, nickname, domain, False, False, proxyType, args.port, httpPrefix, debug, - __version__) + __version__, args.language) sys.exit() if args.json: @@ -892,6 +961,8 @@ if not args.language: languageCode = getConfigParam(baseDir, 'language') if languageCode: args.language = languageCode + else: + args.language = 'en' # maximum number of new registrations if not args.maxRegistrations: @@ -1095,7 +1166,6 @@ if args.message: toDomain = 'public' toPort = port - # ccUrl = httpPrefix + '://' + domain + '/users/' + nickname + '/followers' ccUrl = None sendMessage = args.message followersOnly = args.followersonly @@ -1124,7 +1194,8 @@ if args.message: args.commentsEnabled, attach, mediaType, attachedImageDescription, city, cachedWebfingers, personCache, isArticle, - args.debug, replyTo, replyTo, subject) + args.language, args.lowBandwidth, args.debug, + replyTo, replyTo, args.conversationId, subject) for i in range(10): # TODO detect send success/fail time.sleep(1) @@ -1214,6 +1285,10 @@ if args.itemName: 'with the --summary option') sys.exit() + if not args.itemQty: + print('Specify a quantity of shared items with the --itemQty option') + sys.exit() + if not args.itemType: print('Specify a type of shared item with the --itemType option') sys.exit() @@ -1224,7 +1299,7 @@ if args.itemName: sys.exit() if not args.location: - print('Specify a location or city where theshared ' + + print('Specify a location or city where the shared ' + 'item resides with the --location option') sys.exit() @@ -1245,12 +1320,14 @@ if args.itemName: args.itemName, args.summary, args.itemImage, + args.itemQty, args.itemType, args.itemCategory, args.location, args.duration, cachedWebfingers, personCache, - debug, __version__) + debug, __version__, + args.itemPrice, args.itemCurrency) for i in range(10): # TODO detect send success/fail time.sleep(1) @@ -1285,6 +1362,100 @@ if args.undoItemName: time.sleep(1) sys.exit() +if args.wantedItemName: + if not args.password: + args.password = getpass.getpass('Password: ') + if not args.password: + print('Specify a password with the --password option') + sys.exit() + args.password = args.password.replace('\n', '') + + if not args.nickname: + print('Specify a nickname with the --nickname option') + sys.exit() + + if not args.summary: + print('Specify a description for your shared item ' + + 'with the --summary option') + sys.exit() + + if not args.itemQty: + print('Specify a quantity of shared items with the --itemQty option') + sys.exit() + + if not args.itemType: + print('Specify a type of shared item with the --itemType option') + sys.exit() + + if not args.itemCategory: + print('Specify a category of shared item ' + + 'with the --itemCategory option') + sys.exit() + + if not args.location: + print('Specify a location or city where the wanted ' + + 'item resides with the --location option') + sys.exit() + + if not args.duration: + print('Specify a duration to share the object ' + + 'with the --duration option') + sys.exit() + + session = createSession(proxyType) + personCache = {} + cachedWebfingers = {} + print('Sending wanted item: ' + args.wantedItemName) + + sendWantedViaServer(baseDir, session, + args.nickname, args.password, + domain, port, + httpPrefix, + args.wantedItemName, + args.summary, + args.itemImage, + args.itemQty, + args.itemType, + args.itemCategory, + args.location, + args.duration, + cachedWebfingers, personCache, + debug, __version__, + args.itemPrice, args.itemCurrency) + for i in range(10): + # TODO detect send success/fail + time.sleep(1) + sys.exit() + +if args.undoWantedItemName: + if not args.password: + args.password = getpass.getpass('Password: ') + if not args.password: + print('Specify a password with the --password option') + sys.exit() + args.password = args.password.replace('\n', '') + + if not args.nickname: + print('Specify a nickname with the --nickname option') + sys.exit() + + session = createSession(proxyType) + personCache = {} + cachedWebfingers = {} + print('Sending undo of wanted item: ' + args.undoWantedItemName) + + sendUndoWantedViaServer(baseDir, session, + args.nickname, args.password, + domain, port, + httpPrefix, + args.undoWantedItemName, + cachedWebfingers, personCache, + debug, __version__) + for i in range(10): + # TODO detect send success/fail + time.sleep(1) + sys.exit() + if args.like: if not args.nickname: print('Specify a nickname with the --nickname option') @@ -1674,6 +1845,10 @@ if args.followers: nickname = args.followers.split('/u/')[1] nickname = nickname.replace('\n', '').replace('\r', '') domain = args.followers.split('/u/')[0] + elif '/c/' in args.followers: + nickname = args.followers.split('/c/')[1] + nickname = nickname.replace('\n', '').replace('\r', '') + domain = args.followers.split('/c/')[0] else: # format: @nick@domain if '@' not in args.followers: @@ -1710,7 +1885,7 @@ if args.followers: handle = nickname + '@' + domain wfRequest = webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - None, __version__, debug) + None, __version__, debug, False) if not wfRequest: print('Unable to webfinger ' + handle) sys.exit() @@ -1739,6 +1914,7 @@ if args.followers: personUrl = personUrl.replace('/channel/', '/actor/') personUrl = personUrl.replace('/profile/', '/actor/') personUrl = personUrl.replace('/u/', '/actor/') + personUrl = personUrl.replace('/c/', '/actor/') if not personUrl: # try single user instance personUrl = httpPrefix + '://' + domain @@ -1817,6 +1993,9 @@ if args.addgroup: if not args.domain or not getConfigParam(baseDir, 'domain'): print('Use the --domain option to set the domain name') sys.exit() + if nickname.startswith('!'): + # remove preceding group indicator + nickname = nickname[1:] if not validNickname(domain, nickname): print(nickname + ' is a reserved name. Use something different.') sys.exit() @@ -2090,11 +2269,27 @@ if args.desktop: storeInboxPosts, args.notifyShowNewPosts, args.language, - args.debug) + args.debug, args.lowBandwidth) sys.exit() if federationList: print('Federating with: ' + str(federationList)) +if args.sharedItemsFederatedDomains: + print('Federating shared items with: ' + + args.sharedItemsFederatedDomains) + +sharedItemsFederatedDomains = [] +if args.sharedItemsFederatedDomains: + sharedItemsFederatedDomainsStr = args.sharedItemsFederatedDomains + setConfigParam(baseDir, 'sharedItemsFederatedDomains', + sharedItemsFederatedDomainsStr) +else: + sharedItemsFederatedDomainsStr = \ + getConfigParam(baseDir, 'sharedItemsFederatedDomains') +if sharedItemsFederatedDomainsStr: + sharedItemsFederatedDomainsList = sharedItemsFederatedDomainsStr.split(',') + for sharedFederatedDomain in sharedItemsFederatedDomainsList: + sharedItemsFederatedDomains.append(sharedFederatedDomain.strip()) if args.block: if not nickname: @@ -2245,6 +2440,7 @@ if args.unfilterStr: sys.exit() if args.testdata: + args.language = 'en' city = 'London, England' nickname = 'testuser567' password = 'boringpassword' @@ -2287,21 +2483,21 @@ if args.testdata: "spanner", "It's a spanner", "img/shares1.png", - "tool", + 1, "tool", "mechanical", - "City", + "City", "0", "GBP", "2 months", - debug, city) + debug, city, args.language, {}, 'shares', args.lowBandwidth) addShare(baseDir, httpPrefix, nickname, domain, port, "witch hat", "Spooky", "img/shares2.png", - "hat", + 1, "hat", "clothing", - "City", + "City", "0", "GBP", "3 months", - debug, city) + debug, city, args.language, {}, 'shares', args.lowBandwidth) deleteAllPosts(baseDir, nickname, domain, 'inbox') deleteAllPosts(baseDir, nickname, domain, 'outbox') @@ -2322,6 +2518,8 @@ if args.testdata: testEventTime = None testLocation = None testIsArticle = False + conversationId = None + lowBandwidth = False createPublicPost(baseDir, nickname, domain, port, httpPrefix, "like this is totally just a #test man", @@ -2334,7 +2532,8 @@ if args.testdata: testInReplyTo, testInReplyToAtomUri, testSubject, testSchedulePost, testEventDate, testEventTime, testLocation, - testIsArticle) + testIsArticle, args.language, conversationId, + lowBandwidth) createPublicPost(baseDir, nickname, domain, port, httpPrefix, "Zoiks!!!", testFollowersOnly, @@ -2346,7 +2545,8 @@ if args.testdata: testInReplyTo, testInReplyToAtomUri, testSubject, testSchedulePost, testEventDate, testEventTime, testLocation, - testIsArticle) + testIsArticle, args.language, conversationId, + lowBandwidth) createPublicPost(baseDir, nickname, domain, port, httpPrefix, "Hey scoob we need like a hundred more #milkshakes", testFollowersOnly, @@ -2358,7 +2558,8 @@ if args.testdata: testInReplyTo, testInReplyToAtomUri, testSubject, testSchedulePost, testEventDate, testEventTime, testLocation, - testIsArticle) + testIsArticle, args.language, conversationId, + lowBandwidth) createPublicPost(baseDir, nickname, domain, port, httpPrefix, "Getting kinda spooky around here", testFollowersOnly, @@ -2370,7 +2571,8 @@ if args.testdata: 'someone', testInReplyToAtomUri, testSubject, testSchedulePost, testEventDate, testEventTime, testLocation, - testIsArticle) + testIsArticle, args.language, conversationId, + lowBandwidth) createPublicPost(baseDir, nickname, domain, port, httpPrefix, "And they would have gotten away with it too" + "if it wasn't for those pesky hackers", @@ -2383,7 +2585,8 @@ if args.testdata: testInReplyTo, testInReplyToAtomUri, testSubject, testSchedulePost, testEventDate, testEventTime, testLocation, - testIsArticle) + testIsArticle, args.language, conversationId, + lowBandwidth) createPublicPost(baseDir, nickname, domain, port, httpPrefix, "man these centralized sites are like the worst!", testFollowersOnly, @@ -2395,7 +2598,8 @@ if args.testdata: testInReplyTo, testInReplyToAtomUri, testSubject, testSchedulePost, testEventDate, testEventTime, testLocation, - testIsArticle) + testIsArticle, args.language, conversationId, + lowBandwidth) createPublicPost(baseDir, nickname, domain, port, httpPrefix, "another mystery solved #test", testFollowersOnly, @@ -2407,7 +2611,8 @@ if args.testdata: testInReplyTo, testInReplyToAtomUri, testSubject, testSchedulePost, testEventDate, testEventTime, testLocation, - testIsArticle) + testIsArticle, args.language, conversationId, + lowBandwidth) createPublicPost(baseDir, nickname, domain, port, httpPrefix, "let's go bowling", testFollowersOnly, @@ -2419,21 +2624,22 @@ if args.testdata: testInReplyTo, testInReplyToAtomUri, testSubject, testSchedulePost, testEventDate, testEventTime, testLocation, - testIsArticle) + testIsArticle, args.language, conversationId, + lowBandwidth) domainFull = domain + ':' + str(port) clearFollows(baseDir, nickname, domain) followPerson(baseDir, nickname, domain, 'maxboardroom', domainFull, - federationList, False) + federationList, False, False) followPerson(baseDir, nickname, domain, 'ultrapancake', domainFull, - federationList, False) + federationList, False, False) followPerson(baseDir, nickname, domain, 'sausagedog', domainFull, - federationList, False) + federationList, False, False) followPerson(baseDir, nickname, domain, 'drokk', domainFull, - federationList, False) + federationList, False, False) followerOfPerson(baseDir, nickname, domain, 'drokk', domainFull, - federationList, False) + federationList, False, False) followerOfPerson(baseDir, nickname, domain, 'maxboardroom', domainFull, - federationList, False) + federationList, False, False) setConfigParam(baseDir, 'admin', nickname) # set a lower bound to the maximum mentions @@ -2506,6 +2712,11 @@ sendThreadsTimeoutMins = \ if sendThreadsTimeoutMins is not None: args.sendThreadsTimeoutMins = int(sendThreadsTimeoutMins) +maxLikeCount = \ + getConfigParam(baseDir, 'maxLikeCount') +if maxLikeCount is not None: + args.maxLikeCount = int(maxLikeCount) + showPublishAsIcon = \ getConfigParam(baseDir, 'showPublishAsIcon') if showPublishAsIcon is not None: @@ -2561,6 +2772,11 @@ showNodeInfoVersion = \ if showNodeInfoVersion is not None: args.showNodeInfoVersion = bool(showNodeInfoVersion) +lowBandwidth = \ + getConfigParam(baseDir, 'lowBandwidth') +if lowBandwidth is not None: + args.lowBandwidth = bool(lowBandwidth) + userAgentsBlocked = [] if args.userAgentBlocks: userAgentsBlockedStr = args.userAgentBlocks @@ -2608,8 +2824,18 @@ if args.registration: setConfigParam(baseDir, 'registration', 'closed') print('New registrations closed') +defaultCurrency = getConfigParam(baseDir, 'defaultCurrency') +if not defaultCurrency: + setConfigParam(baseDir, 'defaultCurrency', 'EUR') +if args.defaultCurrency: + if args.defaultCurrency == args.defaultCurrency.upper(): + setConfigParam(baseDir, 'defaultCurrency', args.defaultCurrency) + print('Default currency set to ' + args.defaultCurrency) + if __name__ == "__main__": - runDaemon(userAgentsBlocked, + runDaemon(args.lowBandwidth, args.maxLikeCount, + sharedItemsFederatedDomains, + userAgentsBlocked, args.logLoginFailures, args.city, args.showNodeInfoAccounts, diff --git a/filters.py b/filters.py index 581f75eb0..875fb5503 100644 --- a/filters.py +++ b/filters.py @@ -119,14 +119,22 @@ def _isFilteredBase(filename: str, content: str) -> bool: return False +def isFilteredGlobally(baseDir: str, content: str) -> bool: + """Is the given content globally filtered? + """ + globalFiltersFilename = baseDir + '/accounts/filters.txt' + if _isFilteredBase(globalFiltersFilename, content): + return True + return False + + def isFiltered(baseDir: str, nickname: str, domain: str, content: str) -> bool: """Should the given content be filtered out? This is a simple type of filter which just matches words, not a regex You can add individual words or use word1+word2 to indicate that two words must be present although not necessarily adjacent """ - globalFiltersFilename = baseDir + '/accounts/filters.txt' - if _isFilteredBase(globalFiltersFilename, content): + if isFilteredGlobally(baseDir, content): return True if not nickname or not domain: diff --git a/follow.py b/follow.py index 2654989e4..b81786db3 100644 --- a/follow.py +++ b/follow.py @@ -28,12 +28,16 @@ from utils import saveJson from utils import isAccountDir from utils import getUserPaths from utils import acctDir +from utils import hasGroupType +from utils import isGroupAccount +from utils import localActorUrl from acceptreject import createAccept from acceptreject import createReject from webfinger import webfingerHandle from auth import createBasicAuthHeader from session import getJson from session import postJson +from cache import getPersonPubKey def createInitialLastSeen(baseDir: str, httpPrefix: str) -> None: @@ -62,11 +66,11 @@ def createInitialLastSeen(baseDir: str, httpPrefix: str) -> None: handle = handle.replace('\n', '') nickname = handle.split('@')[0] domain = handle.split('@')[1] - actor = \ - httpPrefix + '://' + domain + '/users/' + nickname + if nickname.startswith('!'): + nickname = nickname[1:] + actor = localActorUrl(httpPrefix, nickname, domain) lastSeenFilename = \ lastSeenDir + '/' + actor.replace('/', '#') + '.txt' - print('lastSeenFilename: ' + lastSeenFilename) if not os.path.isfile(lastSeenFilename): with open(lastSeenFilename, 'w+') as fp: fp.write(str(100)) @@ -108,12 +112,11 @@ def _removeFromFollowBase(baseDir: str, acceptDenyDomain = acceptOrDenyHandle.split('@')[1] # for each possible users path construct an actor and # check if it exists in teh file - usersPaths = ('users', 'profile', 'channel', 'accounts', 'u') + usersPaths = getUserPaths() actorFound = False for usersName in usersPaths: acceptDenyActor = \ - '://' + acceptDenyDomain + '/' + \ - usersName + '/' + acceptDenyNickname + '://' + acceptDenyDomain + usersName + acceptDenyNickname if acceptDenyActor in open(approveFollowsFilename).read(): actorFound = True break @@ -195,12 +198,13 @@ def getMutualsOfPerson(baseDir: str, def followerOfPerson(baseDir: str, nickname: str, domain: str, followerNickname: str, followerDomain: str, - federationList: [], debug: bool) -> bool: + federationList: [], debug: bool, + groupAccount: bool) -> bool: """Adds a follower of the given person """ return followPerson(baseDir, nickname, domain, followerNickname, followerDomain, - federationList, debug, 'followers.txt') + federationList, debug, groupAccount, 'followers.txt') def isFollowerOfPerson(baseDir: str, nickname: str, domain: str, @@ -234,13 +238,15 @@ def isFollowerOfPerson(baseDir: str, nickname: str, domain: str, def unfollowAccount(baseDir: str, nickname: str, domain: str, followNickname: str, followDomain: str, - followFile: str = 'following.txt', - debug: bool = False) -> bool: + debug: bool, groupAccount: bool, + followFile: str = 'following.txt') -> bool: """Removes a person to the follow list """ domain = removeDomainPort(domain) handle = nickname + '@' + domain handleToUnfollow = followNickname + '@' + followDomain + if groupAccount: + handleToUnfollow = '!' + handleToUnfollow if not os.path.isdir(baseDir + '/accounts'): os.mkdir(baseDir + '/accounts') if not os.path.isdir(baseDir + '/accounts/' + handle): @@ -261,8 +267,9 @@ def unfollowAccount(baseDir: str, nickname: str, domain: str, lines = f.readlines() with open(filename, 'w+') as f: for line in lines: - if line.strip("\n").strip("\r").lower() != \ - handleToUnfollowLower: + checkHandle = line.strip("\n").strip("\r").lower() + if checkHandle != handleToUnfollowLower and \ + checkHandle != '!' + handleToUnfollowLower: f.write(line) # write to an unfollowed file so that if a follow accept @@ -282,16 +289,16 @@ def unfollowAccount(baseDir: str, nickname: str, domain: str, def unfollowerOfAccount(baseDir: str, nickname: str, domain: str, followerNickname: str, followerDomain: str, - debug: bool = False) -> bool: + debug: bool, groupAccount: bool) -> bool: """Remove a follower of a person """ return unfollowAccount(baseDir, nickname, domain, followerNickname, followerDomain, - 'followers.txt', debug) + debug, groupAccount, 'followers.txt') def clearFollows(baseDir: str, nickname: str, domain: str, - followFile='following.txt') -> None: + followFile: str = 'following.txt') -> None: """Removes all follows """ handle = nickname + '@' + domain @@ -392,11 +399,10 @@ def getFollowingFeed(baseDir: str, domain: str, port: int, path: str, if headerOnly: firstStr = \ - httpPrefix + '://' + domain + '/users/' + \ - nickname + '/' + followFile + '?page=1' + localActorUrl(httpPrefix, nickname, domain) + \ + '/' + followFile + '?page=1' idStr = \ - httpPrefix + '://' + domain + '/users/' + \ - nickname + '/' + followFile + localActorUrl(httpPrefix, nickname, domain) + '/' + followFile totalStr = \ _getNoOfFollows(baseDir, nickname, domain, authorized) following = { @@ -413,10 +419,10 @@ def getFollowingFeed(baseDir: str, domain: str, port: int, path: str, nextPageNumber = int(pageNumber + 1) idStr = \ - httpPrefix + '://' + domain + '/users/' + \ - nickname + '/' + followFile + '?page=' + str(pageNumber) + localActorUrl(httpPrefix, nickname, domain) + \ + '/' + followFile + '?page=' + str(pageNumber) partOfStr = \ - httpPrefix + '://' + domain + '/users/' + nickname + '/' + followFile + localActorUrl(httpPrefix, nickname, domain) + '/' + followFile following = { '@context': 'https://www.w3.org/ns/activitystreams', 'id': idStr, @@ -446,10 +452,14 @@ def getFollowingFeed(baseDir: str, domain: str, port: int, path: str, if currPage == pageNumber: line2 = \ line.lower().replace('\n', '').replace('\r', '') - url = httpPrefix + '://' + \ - line2.split('@')[1] + \ - '/users/' + \ - line2.split('@')[0] + nick = line2.split('@')[0] + dom = line2.split('@')[1] + if not nick.startswith('!'): + # person actor + url = localActorUrl(httpPrefix, nick, dom) + else: + # group actor + url = httpPrefix + '://' + dom + '/c/' + nick following['orderedItems'].append(url) elif ((line.startswith('http') or line.startswith('hyper')) and @@ -470,8 +480,8 @@ def getFollowingFeed(baseDir: str, domain: str, port: int, path: str, lastPage = 1 if nextPageNumber > lastPage: following['next'] = \ - httpPrefix + '://' + domain + '/users/' + \ - nickname + '/' + followFile + '?page=' + str(lastPage) + localActorUrl(httpPrefix, nickname, domain) + \ + '/' + followFile + '?page=' + str(lastPage) return following @@ -535,7 +545,8 @@ def _storeFollowRequest(baseDir: str, nicknameToFollow: str, domainToFollow: str, port: int, nickname: str, domain: str, fromPort: int, followJson: {}, - debug: bool, personUrl: str) -> bool: + debug: bool, personUrl: str, + groupAccount: bool) -> bool: """Stores the follow request for later use """ accountsDir = baseDir + '/accounts/' + \ @@ -543,10 +554,12 @@ def _storeFollowRequest(baseDir: str, if not os.path.isdir(accountsDir): return False - approveHandle = nickname + '@' + domain domainFull = getFullDomain(domain, fromPort) approveHandle = getFullDomain(nickname + '@' + domain, fromPort) + if groupAccount: + approveHandle = '!' + approveHandle + followersFilename = accountsDir + '/followers.txt' if os.path.isfile(followersFilename): alreadyFollowing = False @@ -557,14 +570,13 @@ def _storeFollowRequest(baseDir: str, if approveHandle in followersStr: alreadyFollowing = True - elif '://' + domainFull + '/profile/' + nickname in followersStr: - alreadyFollowing = True - elif '://' + domainFull + '/channel/' + nickname in followersStr: - alreadyFollowing = True - elif '://' + domainFull + '/accounts/' + nickname in followersStr: - alreadyFollowing = True - elif '://' + domainFull + '/u/' + nickname in followersStr: - alreadyFollowing = True + else: + usersPaths = getUserPaths() + for possibleUsersPath in usersPaths: + url = '://' + domainFull + possibleUsersPath + nickname + if url in followersStr: + alreadyFollowing = True + break if alreadyFollowing: if debug: @@ -590,6 +602,8 @@ def _storeFollowRequest(baseDir: str, approveHandleStored = approveHandle if '/users/' not in personUrl: approveHandleStored = personUrl + if groupAccount: + approveHandle = '!' + approveHandle if os.path.isfile(approveFollowsFilename): if approveHandle not in open(approveFollowsFilename).read(): @@ -617,7 +631,7 @@ def receiveFollowRequest(session, baseDir: str, httpPrefix: str, cachedWebfingers: {}, personCache: {}, messageJson: {}, federationList: [], debug: bool, projectVersion: str, - maxFollowers: int) -> bool: + maxFollowers: int, onionDomain: str) -> bool: """Receives a follow request within the POST section of HTTPServer """ if not messageJson['type'].startswith('Follow'): @@ -723,36 +737,78 @@ def receiveFollowRequest(session, baseDir: str, httpPrefix: str, print('Too many follow requests') return False + # Get the actor for the follower and add it to the cache. + # Getting their public key has the same result + if debug: + print('Obtaining the following actor: ' + messageJson['actor']) + if not getPersonPubKey(baseDir, session, messageJson['actor'], + personCache, debug, projectVersion, + httpPrefix, domainToFollow, onionDomain): + if debug: + print('Unable to obtain following actor: ' + + messageJson['actor']) + + groupAccount = \ + hasGroupType(baseDir, messageJson['actor'], personCache) + if groupAccount and isGroupAccount(baseDir, nickname, domain): + print('Group cannot follow a group') + return False + print('Storing follow request for approval') return _storeFollowRequest(baseDir, nicknameToFollow, domainToFollow, port, nickname, domain, fromPort, - messageJson, debug, messageJson['actor']) + messageJson, debug, messageJson['actor'], + groupAccount) else: - print('Follow request does not require approval') + print('Follow request does not require approval ' + approveHandle) # update the followers - if os.path.isdir(baseDir + '/accounts/' + - nicknameToFollow + '@' + domainToFollow): - followersFilename = \ - baseDir + '/accounts/' + \ - nicknameToFollow + '@' + domainToFollow + '/followers.txt' + accountToBeFollowed = \ + acctDir(baseDir, nicknameToFollow, domainToFollow) + if os.path.isdir(accountToBeFollowed): + followersFilename = accountToBeFollowed + '/followers.txt' # for actors which don't follow the mastodon # /users/ path convention store the full actor if '/users/' not in messageJson['actor']: approveHandle = messageJson['actor'] + # Get the actor for the follower and add it to the cache. + # Getting their public key has the same result + if debug: + print('Obtaining the following actor: ' + messageJson['actor']) + if not getPersonPubKey(baseDir, session, messageJson['actor'], + personCache, debug, projectVersion, + httpPrefix, domainToFollow, onionDomain): + if debug: + print('Unable to obtain following actor: ' + + messageJson['actor']) + print('Updating followers file: ' + followersFilename + ' adding ' + approveHandle) if os.path.isfile(followersFilename): if approveHandle not in open(followersFilename).read(): + groupAccount = \ + hasGroupType(baseDir, + messageJson['actor'], personCache) + if debug: + print(approveHandle + ' / ' + messageJson['actor'] + + ' is Group: ' + str(groupAccount)) + if groupAccount and \ + isGroupAccount(baseDir, nickname, domain): + print('Group cannot follow a group') + return False try: with open(followersFilename, 'r+') as followersFile: content = followersFile.read() if approveHandle + '\n' not in content: followersFile.seek(0, 0) - followersFile.write(approveHandle + '\n' + - content) + if not groupAccount: + followersFile.write(approveHandle + + '\n' + content) + else: + followersFile.write('!' + approveHandle + + '\n' + content) except Exception as e: print('WARN: ' + 'Failed to write entry to followers file ' + @@ -815,13 +871,20 @@ def followedAccountAccepts(session, baseDir: str, httpPrefix: str, except BaseException: pass + groupAccount = False + if followJson: + if followJson.get('actor'): + if hasGroupType(baseDir, followJson['actor'], personCache): + groupAccount = True + return sendSignedJson(acceptJson, session, baseDir, nicknameToFollow, domainToFollow, port, nickname, domain, fromPort, '', httpPrefix, True, clientToServer, federationList, sendThreads, postLog, cachedWebfingers, - personCache, debug, projectVersion) + personCache, debug, projectVersion, None, + groupAccount) def followedAccountRejects(session, baseDir: str, httpPrefix: str, @@ -867,6 +930,9 @@ def followedAccountRejects(session, baseDir: str, httpPrefix: str, nickname + '@' + domain + ' port ' + str(fromPort)) clientToServer = False denyHandle = getFullDomain(nickname + '@' + domain, fromPort) + groupAccount = False + if hasGroupType(baseDir, personUrl, personCache): + groupAccount = True # remove from the follow requests file removeFromFollowRequests(baseDir, nicknameToFollow, domainToFollow, denyHandle, debug) @@ -882,7 +948,8 @@ def followedAccountRejects(session, baseDir: str, httpPrefix: str, httpPrefix, True, clientToServer, federationList, sendThreads, postLog, cachedWebfingers, - personCache, debug, projectVersion) + personCache, debug, projectVersion, None, + groupAccount) def sendFollowRequest(session, baseDir: str, @@ -901,15 +968,20 @@ def sendFollowRequest(session, baseDir: str, return None fullDomain = getFullDomain(domain, port) - followActor = httpPrefix + '://' + fullDomain + '/users/' + nickname + followActor = localActorUrl(httpPrefix, nickname, fullDomain) requestDomain = getFullDomain(followDomain, followPort) statusNumber, published = getStatusNumber() + groupAccount = False if followNickname: followedId = followedActor followHandle = followNickname + '@' + requestDomain + groupAccount = hasGroupType(baseDir, followedActor, personCache) + if groupAccount: + followHandle = '!' + followHandle + print('Follow request being sent to group account') else: if debug: print('DEBUG: sendFollowRequest - assuming single user instance') @@ -924,6 +996,9 @@ def sendFollowRequest(session, baseDir: str, 'actor': followActor, 'object': followedId } + if groupAccount: + newFollowJson['to'] = followedId + print('Follow request: ' + str(newFollowJson)) if _followApprovalRequired(baseDir, nickname, domain, debug, followHandle): @@ -941,7 +1016,7 @@ def sendFollowRequest(session, baseDir: str, httpPrefix, True, clientToServer, federationList, sendThreads, postLog, cachedWebfingers, personCache, - debug, projectVersion) + debug, projectVersion, None, groupAccount) return newFollowJson @@ -964,10 +1039,9 @@ def sendFollowRequestViaServer(baseDir: str, session, followDomainFull = getFullDomain(followDomain, followPort) - followActor = httpPrefix + '://' + \ - fromDomainFull + '/users/' + fromNickname - followedId = httpPrefix + '://' + \ - followDomainFull + '/users/' + followNickname + followActor = localActorUrl(httpPrefix, fromNickname, fromDomainFull) + followedId = \ + httpPrefix + '://' + followDomainFull + '/@' + followNickname statusNumber, published = getStatusNumber() newFollowJson = { @@ -983,7 +1057,7 @@ def sendFollowRequestViaServer(baseDir: str, session, # lookup the inbox for the To handle wfRequest = \ webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - fromDomain, projectVersion, debug) + fromDomain, projectVersion, debug, False) if not wfRequest: if debug: print('DEBUG: follow request webfinger failed for ' + handle) @@ -1050,10 +1124,9 @@ def sendUnfollowRequestViaServer(baseDir: str, session, fromDomainFull = getFullDomain(fromDomain, fromPort) followDomainFull = getFullDomain(followDomain, followPort) - followActor = httpPrefix + '://' + \ - fromDomainFull + '/users/' + fromNickname - followedId = httpPrefix + '://' + \ - followDomainFull + '/users/' + followNickname + followActor = localActorUrl(httpPrefix, fromNickname, fromDomainFull) + followedId = \ + httpPrefix + '://' + followDomainFull + '/@' + followNickname statusNumber, published = getStatusNumber() unfollowJson = { @@ -1074,7 +1147,7 @@ def sendUnfollowRequestViaServer(baseDir: str, session, # lookup the inbox for the To handle wfRequest = \ webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - fromDomain, projectVersion, debug) + fromDomain, projectVersion, debug, False) if not wfRequest: if debug: print('DEBUG: unfollow webfinger failed for ' + handle) @@ -1140,7 +1213,7 @@ def getFollowingViaServer(baseDir: str, session, return 6 domainFull = getFullDomain(domain, port) - followActor = httpPrefix + '://' + domainFull + '/users/' + nickname + followActor = localActorUrl(httpPrefix, nickname, domainFull) authHeader = createBasicAuthHeader(nickname, password) @@ -1181,7 +1254,7 @@ def getFollowersViaServer(baseDir: str, session, return 6 domainFull = getFullDomain(domain, port) - followActor = httpPrefix + '://' + domainFull + '/users/' + nickname + followActor = localActorUrl(httpPrefix, nickname, domainFull) authHeader = createBasicAuthHeader(nickname, password) @@ -1222,7 +1295,7 @@ def getFollowRequestsViaServer(baseDir: str, session, domainFull = getFullDomain(domain, port) - followActor = httpPrefix + '://' + domainFull + '/users/' + nickname + followActor = localActorUrl(httpPrefix, nickname, domainFull) authHeader = createBasicAuthHeader(nickname, password) headers = { @@ -1263,7 +1336,7 @@ def approveFollowRequestViaServer(baseDir: str, session, return 6 domainFull = getFullDomain(domain, port) - actor = httpPrefix + '://' + domainFull + '/users/' + nickname + actor = localActorUrl(httpPrefix, nickname, domainFull) authHeader = createBasicAuthHeader(nickname, password) @@ -1303,7 +1376,7 @@ def denyFollowRequestViaServer(baseDir: str, session, return 6 domainFull = getFullDomain(domain, port) - actor = httpPrefix + '://' + domainFull + '/users/' + nickname + actor = localActorUrl(httpPrefix, nickname, domainFull) authHeader = createBasicAuthHeader(nickname, password) @@ -1417,8 +1490,10 @@ def outboxUndoFollow(baseDir: str, messageJson: {}, debug: bool) -> None: getDomainFromActor(messageJson['object']['object']) domainFollowingFull = getFullDomain(domainFollowing, portFollowing) + groupAccount = hasGroupType(baseDir, messageJson['object']['object'], None) if unfollowAccount(baseDir, nicknameFollower, domainFollowerFull, - nicknameFollowing, domainFollowingFull): + nicknameFollowing, domainFollowingFull, + debug, groupAccount): if debug: print('DEBUG: ' + nicknameFollower + ' unfollowed ' + nicknameFollowing + '@' + domainFollowingFull) diff --git a/followingCalendar.py b/followingCalendar.py index 9f038899e..e5453eb2b 100644 --- a/followingCalendar.py +++ b/followingCalendar.py @@ -13,6 +13,7 @@ import os def _dirAcct(baseDir: str, nickname: str, domain: str) -> str: return baseDir + '/accounts/' + nickname + '@' + domain + def _portDomainRemove(domain: str) -> str: """If the domain has a port appended then remove it eg. mydomain.com:80 becomes mydomain.com diff --git a/gemini/EN/index.gmi b/gemini/EN/index.gmi index aab2ee499..531156739 100644 --- a/gemini/EN/index.gmi +++ b/gemini/EN/index.gmi @@ -21,8 +21,8 @@ Epicyon is written in Python with a HTML+CSS web interface and uses no javascrip Emojis, hashtags, photos, video and audio attachments, instance and account level blocking controls, moderation functions and reports are all supported. Build the community you want and avoid the stuff you don't. No ads. No blockchains or other Silicon Valley garbage. -=> https://epicyon.net/epicyon.tar.gz Download -=> https://epicyon.net/#install Install +=> https://libreserver.org/epicyon/epicyon.tar.gz Download +=> https://libreserver.org/epicyon/#install Install => https://gitlab.com/bashrc2/epicyon Repo => https://www.patreon.com/freedombone Donate => features.gmi Features diff --git a/happening.py b/happening.py index 8583ac0b2..a3d877528 100644 --- a/happening.py +++ b/happening.py @@ -55,6 +55,9 @@ def saveEventPost(baseDir: str, handle: str, postId: str, See https://framagit.org/framasoft/mobilizon/-/blob/ master/lib/federation/activity_stream/converter/event.ex """ + if not os.path.isdir(baseDir + '/accounts/' + handle): + print('WARN: Account does not exist at ' + + baseDir + '/accounts/' + handle) calendarPath = baseDir + '/accounts/' + handle + '/calendar' if not os.path.isdir(calendarPath): os.mkdir(calendarPath) diff --git a/httpsig.py b/httpsig.py index 45372fc90..50d897e3c 100644 --- a/httpsig.py +++ b/httpsig.py @@ -24,6 +24,7 @@ from time import gmtime, strftime import datetime from utils import getFullDomain from utils import getSHA256 +from utils import localActorUrl def messageContentDigest(messageBodyJsonStr: str) -> str: @@ -48,7 +49,7 @@ def signPostHeaders(dateStr: str, privateKeyPem: str, if not dateStr: dateStr = strftime("%a, %d %b %Y %H:%M:%S %Z", gmtime()) - keyID = httpPrefix + '://' + domain + '/users/' + nickname + '#main-key' + keyID = localActorUrl(httpPrefix, nickname, domain) + '#main-key' if not messageBodyJsonStr: headers = { '(request-target)': f'post {path}', @@ -125,7 +126,7 @@ def signPostHeadersNew(dateStr: str, privateKeyPem: str, currTime = datetime.datetime.strptime(dateStr, timeFormat) secondsSinceEpoch = \ int((currTime - datetime.datetime(1970, 1, 1)).total_seconds()) - keyID = httpPrefix + '://' + domain + '/users/' + nickname + '#main-key' + keyID = localActorUrl(httpPrefix, nickname, domain) + '#main-key' if not messageBodyJsonStr: headers = { '*request-target': f'post {path}', diff --git a/inbox.py b/inbox.py index 1a129dae3..3decbf5ba 100644 --- a/inbox.py +++ b/inbox.py @@ -13,6 +13,9 @@ import datetime import time import random from linked_data_sig import verifyJsonSignature +from languages import understoodPostLanguage +from utils import getUserPaths +from utils import getBaseContentFromPost from utils import acctDir from utils import removeDomainPort from utils import getPortFromDomain @@ -23,7 +26,6 @@ from utils import getConfigParam from utils import hasUsersPath from utils import validPostDate from utils import getFullDomain -from utils import isEventPost from utils import removeIdEnding from utils import getProtocolPrefixes from utils import isBlogPost @@ -43,18 +45,19 @@ from utils import loadJson from utils import saveJson from utils import updateLikesCollection from utils import undoLikesCollectionEntry +from utils import hasGroupType +from utils import localActorUrl from categories import getHashtagCategories from categories import setHashtagCategory from httpsig import verifyPostHeaders from session import createSession -from session import getJson from follow import isFollowingActor from follow import receiveFollowRequest from follow import getFollowersOfActor from follow import unfollowerOfAccount from pprint import pprint -from cache import getPersonFromCache from cache import storePersonInCache +from cache import getPersonPubKey from acceptreject import receiveAcceptReject from bookmarks import updateBookmarksCollection from bookmarks import undoBookmarksCollectionEntry @@ -87,7 +90,9 @@ from categories import guessHashtagCategory from context import hasValidContext from speaker import updateSpeaker from announce import isSelfAnnounce +from announce import createAnnounce from notifyOnPost import notifyWhenPersonPosts +from conversation import updateConversation def storeHashTags(baseDir: str, nickname: str, postJsonObject: {}) -> None: @@ -151,7 +156,7 @@ def storeHashTags(baseDir: str, nickname: str, postJsonObject: {}) -> None: categoryStr = \ guessHashtagCategory(tagName, hashtagCategories) if categoryStr: - setHashtagCategory(baseDir, tagName, categoryStr) + setHashtagCategory(baseDir, tagName, categoryStr, False) def _inboxStorePostToHtmlCache(recentPostsCache: {}, maxRecentPosts: int, @@ -164,7 +169,8 @@ def _inboxStorePostToHtmlCache(recentPostsCache: {}, maxRecentPosts: int, showPublishedDateOnly: bool, peertubeInstances: [], allowLocalNetworkAccess: bool, - themeName: str) -> None: + themeName: str, systemLanguage: str, + maxLikeCount: int) -> None: """Converts the json post into html and stores it in a cache This enables the post to be quickly displayed later """ @@ -182,7 +188,7 @@ def _inboxStorePostToHtmlCache(recentPostsCache: {}, maxRecentPosts: int, httpPrefix, __version__, boxname, None, showPublishedDateOnly, peertubeInstances, allowLocalNetworkAccess, - themeName, + themeName, systemLanguage, maxLikeCount, not isDM(postJsonObject), True, True, False, True) @@ -215,67 +221,25 @@ def validInboxFilenames(baseDir: str, nickname: str, domain: str, domain = removeDomainPort(domain) inboxDir = acctDir(baseDir, nickname, domain) + '/inbox' if not os.path.isdir(inboxDir): + print('Not an inbox directory: ' + inboxDir) return True expectedStr = expectedDomain + ':' + str(expectedPort) + expectedFound = False for subdir, dirs, files in os.walk(inboxDir): for f in files: filename = os.path.join(subdir, f) if not os.path.isfile(filename): print('filename: ' + filename) return False - if expectedStr not in filename: - print('Expected: ' + expectedStr) - print('Invalid filename: ' + filename) - return False + if expectedStr in filename: + expectedFound = True break + if not expectedFound: + print('Expected file was not found: ' + expectedStr) + return False return True -def getPersonPubKey(baseDir: str, session, personUrl: str, - personCache: {}, debug: bool, - projectVersion: str, httpPrefix: str, - domain: str, onionDomain: str) -> str: - if not personUrl: - return None - personUrl = personUrl.replace('#main-key', '') - if personUrl.endswith('/users/inbox'): - if debug: - print('DEBUG: Obtaining public key for shared inbox') - personUrl = personUrl.replace('/users/inbox', '/inbox') - personJson = \ - getPersonFromCache(baseDir, personUrl, personCache, True) - if not personJson: - if debug: - print('DEBUG: Obtaining public key for ' + personUrl) - personDomain = domain - if onionDomain: - if '.onion/' in personUrl: - personDomain = onionDomain - profileStr = 'https://www.w3.org/ns/activitystreams' - asHeader = { - 'Accept': 'application/activity+json; profile="' + profileStr + '"' - } - personJson = \ - getJson(session, personUrl, asHeader, None, debug, - projectVersion, httpPrefix, personDomain) - if not personJson: - return None - pubKey = None - if personJson.get('publicKey'): - if personJson['publicKey'].get('publicKeyPem'): - pubKey = personJson['publicKey']['publicKeyPem'] - else: - if personJson.get('publicKeyPem'): - pubKey = personJson['publicKeyPem'] - - if not pubKey: - if debug: - print('DEBUG: Public key not found for ' + personUrl) - - storePersonInCache(baseDir, personUrl, personJson, personCache, True) - return pubKey - - def inboxMessageHasParams(messageJson: {}) -> bool: """Checks whether an incoming message contains expected parameters """ @@ -351,8 +315,8 @@ def savePostToInboxQueue(baseDir: str, httpPrefix: str, messageBytes: str, httpHeaders: {}, postPath: str, debug: bool, - blockedCache: []) -> str: - """Saves the give json to the inbox queue for the person + blockedCache: [], systemLanguage: str) -> str: + """Saves the given json to the inbox queue for the person keyId specifies the actor sending the post """ if len(messageBytes) > 10240: @@ -415,9 +379,9 @@ def savePostToInboxQueue(baseDir: str, httpPrefix: str, replyNickname + '@' + replyDomain) return None if postJsonObject['object'].get('content'): - if isinstance(postJsonObject['object']['content'], str): - if isFiltered(baseDir, nickname, domain, - postJsonObject['object']['content']): + contentStr = getBaseContentFromPost(postJsonObject, systemLanguage) + if contentStr: + if isFiltered(baseDir, nickname, domain, contentStr): if debug: print('WARN: post was filtered out due to content') return None @@ -438,8 +402,8 @@ def savePostToInboxQueue(baseDir: str, httpPrefix: str, if actor: postId = actor + '/statuses/' + statusNumber else: - postId = httpPrefix + '://' + originalDomain + \ - '/users/' + nickname + '/statuses/' + statusNumber + postId = localActorUrl(httpPrefix, nickname, originalDomain) + \ + '/statuses/' + statusNumber # NOTE: don't change postJsonObject['id'] before signature check @@ -669,10 +633,11 @@ def _receiveUndoFollow(session, baseDir: str, httpPrefix: str, getDomainFromActor(messageJson['object']['object']) domainFollowingFull = getFullDomain(domainFollowing, portFollowing) + groupAccount = hasGroupType(baseDir, messageJson['object']['actor'], None) if unfollowerOfAccount(baseDir, nicknameFollowing, domainFollowingFull, nicknameFollower, domainFollowerFull, - debug): + debug, groupAccount): print(nicknameFollowing + '@' + domainFollowingFull + ': ' 'Follower ' + nicknameFollower + '@' + domainFollowerFull + ' was removed') @@ -712,6 +677,11 @@ def _receiveUndo(session, baseDir: str, httpPrefix: str, if debug: print('DEBUG: ' + messageJson['type'] + ' has no object type') return False + if not isinstance(messageJson['object']['type'], str): + if debug: + print('DEBUG: ' + messageJson['type'] + + ' type within object is not a string') + return False if not messageJson['object'].get('object'): if debug: print('DEBUG: ' + messageJson['type'] + @@ -730,25 +700,6 @@ def _receiveUndo(session, baseDir: str, httpPrefix: str, return False -def _receiveEventPost(recentPostsCache: {}, session, baseDir: str, - httpPrefix: str, domain: str, port: int, - sendThreads: [], postLog: [], cachedWebfingers: {}, - personCache: {}, messageJson: {}, federationList: [], - nickname: str, debug: bool) -> bool: - """Receive a mobilizon-type event activity - See https://framagit.org/framasoft/mobilizon/-/blob/ - master/lib/federation/activity_stream/converter/event.ex - """ - if not isEventPost(messageJson): - return - print('Receiving event: ' + str(messageJson['object'])) - handle = getFullDomain(nickname + '@' + domain, port) - - postId = removeIdEnding(messageJson['id']).replace('/', '#') - - saveEventPost(baseDir, handle, postId, messageJson['object']) - - def _personReceiveUpdate(baseDir: str, domain: str, port: int, updateNickname: str, updateDomain: str, @@ -762,10 +713,10 @@ def _personReceiveUpdate(baseDir: str, ' ' + str(personJson)) domainFull = getFullDomain(domain, port) updateDomainFull = getFullDomain(updateDomain, updatePort) - usersPaths = ('users', 'profile', 'channel', 'accounts', 'u') + usersPaths = getUserPaths() usersStrFound = False for usersStr in usersPaths: - actor = updateDomainFull + '/' + usersStr + '/' + updateNickname + actor = updateDomainFull + usersStr + updateNickname if actor in personJson['id']: usersStrFound = True break @@ -883,6 +834,11 @@ def _receiveUpdate(recentPostsCache: {}, session, baseDir: str, if debug: print('DEBUG: ' + messageJson['type'] + ' object has no type') return False + if not isinstance(messageJson['object']['type'], str): + if debug: + print('DEBUG: ' + messageJson['type'] + + ' object type is not string') + return False if not hasUsersPath(messageJson['actor']): if debug: print('DEBUG: "users" or "profile" missing from actor in ' + @@ -1305,7 +1261,7 @@ def _receiveAnnounce(recentPostsCache: {}, debug: bool, translate: {}, YTReplacementDomain: str, allowLocalNetworkAccess: bool, - themeName: str) -> bool: + themeName: str, systemLanguage: str) -> bool: """Receives an announce activity within the POST section of HTTPServer """ if messageJson['type'] != 'Announce': @@ -1385,6 +1341,7 @@ def _receiveAnnounce(recentPostsCache: {}, if debug: print('DEBUG: Downloading announce post ' + messageJson['actor'] + ' -> ' + messageJson['object']) + domainFull = getFullDomain(domain, port) postJsonObject = downloadAnnounce(session, baseDir, httpPrefix, nickname, domain, @@ -1392,7 +1349,9 @@ def _receiveAnnounce(recentPostsCache: {}, __version__, translate, YTReplacementDomain, allowLocalNetworkAccess, - recentPostsCache, debug) + recentPostsCache, debug, + systemLanguage, + domainFull, personCache) if not postJsonObject: notInOnion = True if onionDomain: @@ -1616,7 +1575,10 @@ def _estimateNumberOfEmoji(content: str) -> int: def _validPostContent(baseDir: str, nickname: str, domain: str, messageJson: {}, maxMentions: int, maxEmoji: int, - allowLocalNetworkAccess: bool, debug: bool) -> bool: + allowLocalNetworkAccess: bool, debug: bool, + systemLanguage: str, + httpPrefix: str, domainFull: str, + personCache: {}) -> bool: """Is the content of a received post valid? Check for bad html Check for hellthreads @@ -1651,27 +1613,27 @@ def _validPostContent(baseDir: str, nickname: str, domain: str, messageJson['object']['content']): return True - if dangerousMarkup(messageJson['object']['content'], - allowLocalNetworkAccess): + contentStr = getBaseContentFromPost(messageJson, systemLanguage) + if dangerousMarkup(contentStr, allowLocalNetworkAccess): if messageJson['object'].get('id'): print('REJECT ARBITRARY HTML: ' + messageJson['object']['id']) print('REJECT ARBITRARY HTML: bad string in post - ' + - messageJson['object']['content']) + contentStr) return False # check (rough) number of mentions - mentionsEst = _estimateNumberOfMentions(messageJson['object']['content']) + mentionsEst = _estimateNumberOfMentions(contentStr) if mentionsEst > maxMentions: if messageJson['object'].get('id'): print('REJECT HELLTHREAD: ' + messageJson['object']['id']) print('REJECT HELLTHREAD: Too many mentions in post - ' + - messageJson['object']['content']) + contentStr) return False - if _estimateNumberOfEmoji(messageJson['object']['content']) > maxEmoji: + if _estimateNumberOfEmoji(contentStr) > maxEmoji: if messageJson['object'].get('id'): print('REJECT EMOJI OVERLOAD: ' + messageJson['object']['id']) print('REJECT EMOJI OVERLOAD: Too many emoji in post - ' + - messageJson['object']['content']) + contentStr) return False # check number of tags if messageJson['object'].get('tag'): @@ -1684,9 +1646,14 @@ def _validPostContent(baseDir: str, nickname: str, domain: str, print('REJECT: Too many tags in post - ' + messageJson['object']['tag']) return False + # check that the post is in a language suitable for this account + if not understoodPostLanguage(baseDir, nickname, domain, + messageJson, systemLanguage, + httpPrefix, domainFull, + personCache): + return False # check for filtered content - if isFiltered(baseDir, nickname, domain, - messageJson['object']['content']): + if isFiltered(baseDir, nickname, domain, contentStr): print('REJECT: content filtered') return False if messageJson['object'].get('inReplyTo'): @@ -1910,90 +1877,69 @@ def _groupHandle(baseDir: str, handle: str) -> bool: return actorJson['type'] == 'Group' -def _getGroupName(baseDir: str, handle: str) -> str: - """Returns the preferred name of a group - """ - actorFile = baseDir + '/accounts/' + handle + '.json' - if not os.path.isfile(actorFile): - return False - actorJson = loadJson(actorFile) - if not actorJson: - return 'Group' - return actorJson['name'] - - def _sendToGroupMembers(session, baseDir: str, handle: str, port: int, postJsonObject: {}, httpPrefix: str, federationList: [], sendThreads: [], postLog: [], cachedWebfingers: {}, - personCache: {}, debug: bool) -> None: + personCache: {}, debug: bool, + systemLanguage: str, + onionDomain: str, i2pDomain: str) -> None: """When a post arrives for a group send it out to the group members """ + if debug: + print('\n\n=========================================================') + print(handle + ' sending to group members') + + sharedItemFederationTokens = {} + sharedItemsFederatedDomains = [] + sharedItemsFederatedDomainsStr = \ + getConfigParam(baseDir, 'sharedItemsFederatedDomains') + if sharedItemsFederatedDomainsStr: + siFederatedDomainsList = \ + sharedItemsFederatedDomainsStr.split(',') + for sharedFederatedDomain in siFederatedDomainsList: + domainStr = sharedFederatedDomain.strip() + sharedItemsFederatedDomains.append(domainStr) + followersFile = baseDir + '/accounts/' + handle + '/followers.txt' if not os.path.isfile(followersFile): return + if not postJsonObject.get('to'): + return if not postJsonObject.get('object'): return - nickname = handle.split('@')[0] -# groupname = _getGroupName(baseDir, handle) + if not hasObjectDict(postJsonObject): + return + nickname = handle.split('@')[0].replace('!', '') domain = handle.split('@')[1] domainFull = getFullDomain(domain, port) - # set sender + groupActor = localActorUrl(httpPrefix, nickname, domainFull) + if groupActor not in postJsonObject['to']: + return cc = '' - sendingActor = postJsonObject['actor'] - sendingActorNickname = getNicknameFromActor(sendingActor) - sendingActorDomain, sendingActorPort = \ - getDomainFromActor(sendingActor) - sendingActorDomainFull = \ - getFullDomain(sendingActorDomain, sendingActorPort) - senderStr = '@' + sendingActorNickname + '@' + sendingActorDomainFull - if not postJsonObject['object']['content'].startswith(senderStr): - postJsonObject['object']['content'] = \ - senderStr + ' ' + postJsonObject['object']['content'] - # add mention to tag list - if not postJsonObject['object']['tag']: - postJsonObject['object']['tag'] = [] - # check if the mention already exists - mentionExists = False - for mention in postJsonObject['object']['tag']: - if mention['type'] == 'Mention': - if mention.get('href'): - if mention['href'] == sendingActor: - mentionExists = True - if not mentionExists: - # add the mention of the original sender - postJsonObject['object']['tag'].append({ - 'href': sendingActor, - 'name': senderStr, - 'type': 'Mention' - }) + nickname = handle.split('@')[0].replace('!', '') - postJsonObject['actor'] = \ - httpPrefix + '://' + domainFull + '/users/' + nickname - postJsonObject['to'] = \ - [postJsonObject['actor'] + '/followers'] - postJsonObject['cc'] = [cc] - postJsonObject['object']['to'] = postJsonObject['to'] - postJsonObject['object']['cc'] = [cc] - # set subject - if not postJsonObject['object'].get('summary'): - postJsonObject['object']['summary'] = 'General Discussion' - domain = removeDomainPort(domain) - with open(followersFile, 'r') as groupMembers: - for memberHandle in groupMembers: - if memberHandle != handle: - memberNickname = memberHandle.split('@')[0] - memberDomain = memberHandle.split('@')[1] - memberPort = port - if ':' in memberDomain: - memberPort = getPortFromDomain(memberDomain) - memberDomain = removeDomainPort(memberDomain) - sendSignedJson(postJsonObject, session, baseDir, - nickname, domain, port, - memberNickname, memberDomain, memberPort, cc, - httpPrefix, False, False, federationList, - sendThreads, postLog, cachedWebfingers, - personCache, debug, __version__) + if debug: + print('Group announce: ' + postJsonObject['object']['id']) + announceJson = \ + createAnnounce(session, baseDir, federationList, + nickname, domain, port, + groupActor + '/followers', cc, + httpPrefix, + postJsonObject['object']['id'], + False, False, + sendThreads, postLog, + personCache, cachedWebfingers, + debug, __version__) + + sendToFollowersThread(session, baseDir, nickname, domain, + onionDomain, i2pDomain, port, + httpPrefix, federationList, + sendThreads, postLog, + cachedWebfingers, personCache, + announceJson, debug, __version__, + sharedItemsFederatedDomains, + sharedItemFederationTokens) def _inboxUpdateCalendar(baseDir: str, handle: str, @@ -2107,7 +2053,7 @@ def _bounceDM(senderPostId: str, session, httpPrefix: str, sendThreads: [], postLog: [], cachedWebfingers: {}, personCache: {}, translate: {}, debug: bool, - lastBounceMessage: []) -> bool: + lastBounceMessage: [], systemLanguage: str) -> bool: """Sends a bounce message back to the sending handle if a DM has been rejected """ @@ -2126,6 +2072,10 @@ def _bounceDM(senderPostId: str, session, httpPrefix: str, lastBounceMessage[0] = currTime senderNickname = sendingHandle.split('@')[0] + groupAccount = False + if sendingHandle.startswith('!'): + sendingHandle = sendingHandle[1:] + groupAccount = True senderDomain = sendingHandle.split('@')[1] senderPort = port if ':' in senderDomain: @@ -2150,6 +2100,8 @@ def _bounceDM(senderPostId: str, session, httpPrefix: str, eventDate = None eventTime = None location = None + conversationId = None + lowBandwidth = False postJsonObject = \ createDirectMessagePost(baseDir, nickname, domain, port, httpPrefix, content, followersOnly, @@ -2159,7 +2111,8 @@ def _bounceDM(senderPostId: str, session, httpPrefix: str, imageDescription, city, inReplyTo, inReplyToAtomUri, subject, debug, schedulePost, - eventDate, eventTime, location) + eventDate, eventTime, location, + systemLanguage, conversationId, lowBandwidth) if not postJsonObject: print('WARN: unable to create bounce message to ' + sendingHandle) return False @@ -2170,7 +2123,7 @@ def _bounceDM(senderPostId: str, session, httpPrefix: str, senderNickname, senderDomain, senderPort, cc, httpPrefix, False, False, federationList, sendThreads, postLog, cachedWebfingers, - personCache, debug, __version__) + personCache, debug, __version__, None, groupAccount) return True @@ -2183,7 +2136,7 @@ def _isValidDM(baseDir: str, nickname: str, domain: str, port: int, personCache: {}, translate: {}, debug: bool, lastBounceMessage: [], - handle: str) -> bool: + handle: str, systemLanguage: str) -> bool: """Is the given message a valid DM? """ if nickname == 'inbox': @@ -2196,8 +2149,8 @@ def _isValidDM(baseDir: str, nickname: str, domain: str, port: int, if not os.path.isfile(followDMsFilename): # dm index will be updated updateIndexList.append('dm') - _dmNotify(baseDir, handle, - httpPrefix + '://' + domain + '/users/' + nickname + '/dm') + actUrl = localActorUrl(httpPrefix, nickname, domain) + _dmNotify(baseDir, handle, actUrl + '/dm') return True # get the file containing following handles @@ -2258,13 +2211,14 @@ def _isValidDM(baseDir: str, nickname: str, domain: str, port: int, cachedWebfingers, personCache, translate, debug, - lastBounceMessage) + lastBounceMessage, + systemLanguage) return False # dm index will be updated updateIndexList.append('dm') - _dmNotify(baseDir, handle, - httpPrefix + '://' + domain + '/users/' + nickname + '/dm') + actUrl = localActorUrl(httpPrefix, nickname, domain) + _dmNotify(baseDir, handle, actUrl + '/dm') return True @@ -2284,7 +2238,8 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int, allowLocalNetworkAccess: bool, peertubeInstances: [], lastBounceMessage: [], - themeName: str) -> bool: + themeName: str, systemLanguage: str, + maxLikeCount: int) -> bool: """ Anything which needs to be done after initial checks have passed """ actor = keyId @@ -2365,7 +2320,7 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int, debug, translate, YTReplacementDomain, allowLocalNetworkAccess, - themeName): + themeName, systemLanguage): if debug: print('DEBUG: Announce accepted from ' + actor) @@ -2412,9 +2367,12 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int, nickname = handle.split('@')[0] jsonObj = None + domainFull = getFullDomain(domain, port) if _validPostContent(baseDir, nickname, domain, postJsonObject, maxMentions, maxEmoji, - allowLocalNetworkAccess, debug): + allowLocalNetworkAccess, debug, + systemLanguage, httpPrefix, + domainFull, personCache): if postJsonObject.get('object'): jsonObj = postJsonObject['object'] @@ -2447,7 +2405,7 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int, return False # replace YouTube links, so they get less tracking data - replaceYouTube(postJsonObject, YTReplacementDomain) + replaceYouTube(postJsonObject, YTReplacementDomain, systemLanguage) # list of indexes to be updated updateIndexList = ['inbox'] @@ -2464,6 +2422,20 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int, # if the votes on a question have changed then # send out an update questionJson['type'] = 'Update' + sharedItemsFederatedDomains = [] + sharedItemFederationTokens = {} + + sharedItemFederationTokens = {} + sharedItemsFederatedDomains = [] + sharedItemsFederatedDomainsStr = \ + getConfigParam(baseDir, 'sharedItemsFederatedDomains') + if sharedItemsFederatedDomainsStr: + siFederatedDomainsList = \ + sharedItemsFederatedDomainsStr.split(',') + for sharedFederatedDomain in siFederatedDomainsList: + domainStr = sharedFederatedDomain.strip() + sharedItemsFederatedDomains.append(domainStr) + sendToFollowersThread(session, baseDir, nickname, domain, onionDomain, i2pDomain, port, @@ -2471,7 +2443,9 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int, sendThreads, postLog, cachedWebfingers, personCache, postJsonObject, debug, - __version__) + __version__, + sharedItemsFederatedDomains, + sharedItemFederationTokens) isReplyToMutedPost = False @@ -2488,28 +2462,34 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int, personCache, translate, debug, lastBounceMessage, - handle): + handle, systemLanguage): return False # get the actor being replied to - domainFull = getFullDomain(domain, port) - actor = httpPrefix + '://' + domainFull + '/users/' + nickname + actor = localActorUrl(httpPrefix, nickname, domainFull) # create a reply notification file if needed if not postIsDM and isReply(postJsonObject, actor): if nickname != 'inbox': # replies index will be updated updateIndexList.append('tlreplies') + + conversationId = None + if postJsonObject['object'].get('conversation'): + conversationId = \ + postJsonObject['object']['conversation'] + if postJsonObject['object'].get('inReplyTo'): inReplyTo = postJsonObject['object']['inReplyTo'] if inReplyTo: if isinstance(inReplyTo, str): if not isMuted(baseDir, nickname, domain, - inReplyTo): + inReplyTo, conversationId): + actUrl = \ + localActorUrl(httpPrefix, + nickname, domain) _replyNotify(baseDir, handle, - httpPrefix + '://' + domain + - '/users/' + nickname + - '/tlreplies') + actUrl + '/tlreplies') else: isReplyToMutedPost = True @@ -2517,7 +2497,8 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int, nickname, domain, postJsonObject, translate, YTReplacementDomain, allowLocalNetworkAccess, - recentPostsCache, debug): + recentPostsCache, debug, systemLanguage, + domainFull, personCache): # media index will be updated updateIndexList.append('tlmedia') if isBlogPost(postJsonObject): @@ -2544,10 +2525,10 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int, if notifyWhenPersonPosts(baseDir, nickname, domain, fromNickname, fromDomainFull): postId = removeIdEnding(jsonObj['id']) + domFull = getFullDomain(domain, port) postLink = \ - httpPrefix + '://' + \ - getFullDomain(domain, port) + \ - '/users/' + nickname + \ + localActorUrl(httpPrefix, + nickname, domFull) + \ '?notifypost=' + postId.replace('/', '-') _notifyPostArrival(baseDir, handle, postLink) @@ -2591,7 +2572,8 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int, showPublishedDateOnly, peertubeInstances, allowLocalNetworkAccess, - themeName) + themeName, systemLanguage, + maxLikeCount) if debug: timeDiff = \ str(int((time.time() - htmlCacheStartTime) * @@ -2600,9 +2582,11 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int, ' post as html to cache in ' + timeDiff + ' mS') + handleName = handle.split('@')[0] + updateConversation(baseDir, handleName, domain, postJsonObject) + _inboxUpdateCalendar(baseDir, handle, postJsonObject) - handleName = handle.split('@')[0] storeHashTags(baseDir, handleName, postJsonObject) # send the post out to group members @@ -2611,7 +2595,8 @@ def _inboxAfterInitial(recentPostsCache: {}, maxRecentPosts: int, postJsonObject, httpPrefix, federationList, sendThreads, postLog, cachedWebfingers, personCache, - debug) + debug, systemLanguage, + onionDomain, i2pDomain) # if the post wasn't saved if not os.path.isfile(destinationFilename): @@ -2850,7 +2835,8 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int, maxFollowers: int, allowLocalNetworkAccess: bool, peertubeInstances: [], verifyAllSignatures: bool, - themeName: str) -> None: + themeName: str, systemLanguage: str, + maxLikeCount: int) -> None: """Processes received items and moves them to the appropriate directories """ @@ -3122,7 +3108,7 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int, queueJson['post'], federationList, debug, projectVersion, - maxFollowers): + maxFollowers, onionDomain): if os.path.isfile(queueFilename): os.remove(queueFilename) if len(queue) > 0: @@ -3147,23 +3133,6 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int, queue.pop(0) continue - if _receiveEventPost(recentPostsCache, session, - baseDir, httpPrefix, - domain, port, - sendThreads, postLog, - cachedWebfingers, - personCache, - queueJson['post'], - federationList, - queueJson['postNickname'], - debug): - print('Queue: Event activity accepted from ' + keyId) - if os.path.isfile(queueFilename): - os.remove(queueFilename) - if len(queue) > 0: - queue.pop(0) - continue - if _receiveUpdate(recentPostsCache, session, baseDir, httpPrefix, domain, port, @@ -3255,7 +3224,8 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int, allowLocalNetworkAccess, peertubeInstances, lastBounceMessage, - themeName) + themeName, systemLanguage, + maxLikeCount) if debug: pprint(queueJson['post']) print('Queue: Queue post accepted') diff --git a/languages.py b/languages.py new file mode 100644 index 000000000..9d0d4022b --- /dev/null +++ b/languages.py @@ -0,0 +1,309 @@ +__filename__ = "languages.py" +__author__ = "Bob Mottram" +__license__ = "AGPL3+" +__version__ = "1.2.0" +__maintainer__ = "Bob Mottram" +__email__ = "bob@freedombone.net" +__status__ = "Production" +__module_group__ = "Core" + +import os +import json +from urllib import request, parse +from utils import getActorLanguagesList +from utils import removeHtml +from utils import hasObjectDict +from utils import getConfigParam +from utils import localActorUrl +from cache import getPersonFromCache + + +def getActorLanguages(actorJson: {}) -> str: + """Returns a string containing languages used by the given actor + """ + langList = getActorLanguagesList(actorJson) + if not langList: + return '' + languagesStr = '' + for lang in langList: + if languagesStr: + languagesStr += ' / ' + lang + else: + languagesStr = lang + return languagesStr + + +def setActorLanguages(baseDir: str, actorJson: {}, languagesStr: str) -> None: + """Sets the languages used by the given actor + """ + separator = ',' + if '/' in languagesStr: + separator = '/' + elif ',' in languagesStr: + separator = ',' + elif ';' in languagesStr: + separator = ';' + elif '+' in languagesStr: + separator = '+' + elif ' ' in languagesStr: + separator = ' ' + langList = languagesStr.lower().split(separator) + langList2 = '' + for lang in langList: + lang = lang.strip() + if baseDir: + languageFilename = baseDir + '/translations/' + lang + '.json' + if os.path.isfile(languageFilename): + if langList2: + langList2 += ', ' + lang.strip() + else: + langList2 += lang.strip() + else: + if langList2: + langList2 += ', ' + lang.strip() + else: + langList2 += lang.strip() + + # remove any existing value + propertyFound = None + for propertyValue in actorJson['attachment']: + if not propertyValue.get('name'): + continue + if not propertyValue.get('type'): + continue + if not propertyValue['name'].lower().startswith('languages'): + continue + propertyFound = propertyValue + break + if propertyFound: + actorJson['attachment'].remove(propertyFound) + + if not langList2: + return + + newLanguages = { + "name": "Languages", + "type": "PropertyValue", + "value": langList2 + } + actorJson['attachment'].append(newLanguages) + + +def understoodPostLanguage(baseDir: str, nickname: str, domain: str, + messageJson: {}, systemLanguage: str, + httpPrefix: str, domainFull: str, + personCache: {}) -> bool: + """Returns true if the post is written in a language + understood by this account + """ + msgObject = messageJson + if hasObjectDict(messageJson): + msgObject = messageJson['object'] + if not msgObject.get('contentMap'): + return True + if not isinstance(msgObject['contentMap'], dict): + return True + if msgObject['contentMap'].get(systemLanguage): + return True + personUrl = localActorUrl(httpPrefix, nickname, domainFull) + actorJson = getPersonFromCache(baseDir, personUrl, personCache, False) + if not actorJson: + print('WARN: unable to load actor to check languages ' + personUrl) + return False + languagesUnderstood = getActorLanguagesList(actorJson) + if not languagesUnderstood: + return True + for lang in languagesUnderstood: + if msgObject['contentMap'].get(lang): + return True + # is the language for this post supported by libretranslate? + libretranslateUrl = getConfigParam(baseDir, "libretranslateUrl") + if libretranslateUrl: + libretranslateApiKey = getConfigParam(baseDir, "libretranslateApiKey") + langList = \ + libretranslateLanguages(libretranslateUrl, libretranslateApiKey) + for lang in langList: + if msgObject['contentMap'].get(lang): + return True + return False + + +def libretranslateLanguages(url: str, apiKey: str = None) -> []: + """Returns a list of supported languages + """ + if not url: + return [] + if not url.endswith('/languages'): + if not url.endswith('/'): + url += "/languages" + else: + url += "languages" + + params = dict() + + if apiKey: + params["api_key"] = apiKey + + urlParams = parse.urlencode(params) + + req = request.Request(url, data=urlParams.encode()) + + response = request.urlopen(req) + + response_str = response.read().decode() + + result = json.loads(response_str) + if not result: + return [] + if not isinstance(result, list): + return [] + + langList = [] + for lang in result: + if not isinstance(lang, dict): + continue + if not lang.get('code'): + continue + langCode = lang['code'] + if len(langCode) != 2: + continue + langList.append(langCode) + langList.sort() + return langList + + +def getLinksFromContent(content: str) -> {}: + """Returns a list of links within the given content + """ + if '' in subsection: + if url not in links: + linkText = subsection.split('>')[1] + if '<' in linkText: + linkText = linkText.split('<')[0] + links[linkText] = url + return links + + +def addLinksToContent(content: str, links: {}) -> str: + """Adds links back into plain text + """ + for linkText, url in links.items(): + urlDesc = url + if linkText.startswith('@') and linkText in content: + content = \ + content.replace(linkText, + '' + + linkText + '') + else: + if len(urlDesc) > 40: + urlDesc = urlDesc[:40] + content += \ + '

' + \ + urlDesc + '

' + return content + + +def libretranslate(url: str, text: str, + source: str, target: str, apiKey: str = None) -> str: + """Translate string using libretranslate + """ + if not url: + return None + + if not url.endswith('/translate'): + if not url.endswith('/'): + url += "/translate" + else: + url += "translate" + + originalText = text + + # get any links from the text + links = getLinksFromContent(text) + + # LibreTranslate doesn't like markup + text = removeHtml(text) + + # remove any links from plain text version of the content + for _, url in links.items(): + text = text.replace(url, '') + + ltParams = { + "q": text, + "source": source, + "target": target + } + + if apiKey: + ltParams["api_key"] = apiKey + + urlParams = parse.urlencode(ltParams) + + req = request.Request(url, data=urlParams.encode()) + try: + response = request.urlopen(req) + except BaseException: + print('Unable to translate: ' + text) + return originalText + + response_str = response.read().decode() + + translatedText = \ + '

' + json.loads(response_str)['translatedText'] + '

' + + # append links form the original text + if links: + translatedText = addLinksToContent(translatedText, links) + return translatedText + + +def autoTranslatePost(baseDir: str, postJsonObject: {}, + systemLanguage: str, translate: {}) -> str: + """Tries to automatically translate the given post + """ + if not hasObjectDict(postJsonObject): + return '' + msgObject = postJsonObject['object'] + if not msgObject.get('contentMap'): + return '' + if not isinstance(msgObject['contentMap'], dict): + return '' + + # is the language for this post supported by libretranslate? + libretranslateUrl = getConfigParam(baseDir, "libretranslateUrl") + if not libretranslateUrl: + return '' + libretranslateApiKey = getConfigParam(baseDir, "libretranslateApiKey") + langList = \ + libretranslateLanguages(libretranslateUrl, libretranslateApiKey) + for lang in langList: + if msgObject['contentMap'].get(lang): + content = msgObject['contentMap'][lang] + translatedText = \ + libretranslate(libretranslateUrl, content, + lang, systemLanguage, + libretranslateApiKey) + if translatedText: + if removeHtml(translatedText) == removeHtml(content): + return content + translatedText = \ + '

' + translate['Translated'].upper() + '

' + \ + translatedText + return translatedText + return '' diff --git a/like.py b/like.py index ad0bff5da..1eddf571d 100644 --- a/like.py +++ b/like.py @@ -18,6 +18,8 @@ from utils import getDomainFromActor from utils import locatePost from utils import updateLikesCollection from utils import undoLikesCollectionEntry +from utils import hasGroupType +from utils import localActorUrl from posts import sendSignedJson from session import postJson from webfinger import webfingerHandle @@ -74,7 +76,7 @@ def _like(recentPostsCache: {}, newLikeJson = { "@context": "https://www.w3.org/ns/activitystreams", 'type': 'Like', - 'actor': httpPrefix + '://' + fullDomain + '/users/' + nickname, + 'actor': localActorUrl(httpPrefix, nickname, fullDomain), 'object': objectUrl } if ccList: @@ -85,13 +87,20 @@ def _like(recentPostsCache: {}, likedPostNickname = None likedPostDomain = None likedPostPort = None + groupAccount = False if actorLiked: likedPostNickname = getNicknameFromActor(actorLiked) likedPostDomain, likedPostPort = getDomainFromActor(actorLiked) + groupAccount = hasGroupType(baseDir, actorLiked, personCache) else: if hasUsersPath(objectUrl): likedPostNickname = getNicknameFromActor(objectUrl) likedPostDomain, likedPostPort = getDomainFromActor(objectUrl) + if '/' + str(likedPostNickname) + '/' in objectUrl: + actorLiked = \ + objectUrl.split('/' + likedPostNickname + '/')[0] + \ + '/' + likedPostNickname + groupAccount = hasGroupType(baseDir, actorLiked, personCache) if likedPostNickname: postFilename = locatePost(baseDir, nickname, domain, objectUrl) @@ -113,7 +122,7 @@ def _like(recentPostsCache: {}, 'https://www.w3.org/ns/activitystreams#Public', httpPrefix, True, clientToServer, federationList, sendThreads, postLog, cachedWebfingers, personCache, - debug, projectVersion) + debug, projectVersion, None, groupAccount) return newLikeJson @@ -131,7 +140,7 @@ def likePost(recentPostsCache: {}, """ likeDomain = getFullDomain(likeDomain, likePort) - actorLiked = httpPrefix + '://' + likeDomain + '/users/' + likeNickname + actorLiked = localActorUrl(httpPrefix, likeNickname, likeDomain) objectUrl = actorLiked + '/statuses/' + str(likeStatusNumber) return _like(recentPostsCache, @@ -155,7 +164,7 @@ def sendLikeViaServer(baseDir: str, session, fromDomainFull = getFullDomain(fromDomain, fromPort) - actor = httpPrefix + '://' + fromDomainFull + '/users/' + fromNickname + actor = localActorUrl(httpPrefix, fromNickname, fromDomainFull) newLikeJson = { "@context": "https://www.w3.org/ns/activitystreams", @@ -169,7 +178,7 @@ def sendLikeViaServer(baseDir: str, session, # lookup the inbox for the To handle wfRequest = webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - fromDomain, projectVersion, debug) + fromDomain, projectVersion, debug, False) if not wfRequest: if debug: print('DEBUG: like webfinger failed for ' + handle) @@ -233,7 +242,7 @@ def sendUndoLikeViaServer(baseDir: str, session, fromDomainFull = getFullDomain(fromDomain, fromPort) - actor = httpPrefix + '://' + fromDomainFull + '/users/' + fromNickname + actor = localActorUrl(httpPrefix, fromNickname, fromDomainFull) newUndoLikeJson = { "@context": "https://www.w3.org/ns/activitystreams", @@ -251,7 +260,7 @@ def sendUndoLikeViaServer(baseDir: str, session, # lookup the inbox for the To handle wfRequest = webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - fromDomain, projectVersion, debug) + fromDomain, projectVersion, debug, False) if not wfRequest: if debug: print('DEBUG: unlike webfinger failed for ' + handle) diff --git a/manualapprove.py b/manualapprove.py index 18b1b320b..907cd4b82 100644 --- a/manualapprove.py +++ b/manualapprove.py @@ -109,7 +109,10 @@ def manualApproveFollowRequest(session, baseDir: str, if approveHandle in approveFollowsStr: exists = True elif '@' in approveHandle: - reqNick = approveHandle.split('@')[0] + groupAccount = False + if approveHandle.startswith('!'): + groupAccount = True + reqNick = approveHandle.split('@')[0].replace('!', '') reqDomain = approveHandle.split('@')[1].strip() reqPrefix = httpPrefix + '://' + reqDomain paths = getUserPaths() @@ -117,6 +120,8 @@ def manualApproveFollowRequest(session, baseDir: str, if reqPrefix + userPath + reqNick in approveFollowsStr: exists = True approveHandleFull = reqPrefix + userPath + reqNick + if groupAccount: + approveHandleFull = '!' + approveHandleFull break if not exists: print('Manual follow accept: ' + approveHandleFull + diff --git a/mastoapiv1.py b/mastoapiv1.py index 93271f86d..b8b85c04f 100644 --- a/mastoapiv1.py +++ b/mastoapiv1.py @@ -140,6 +140,9 @@ def mastoApiV1Response(path: str, callingDomain: str, _getMastoApiV1Account(baseDir, pathNickname, domain) sendJsonStr = 'masto API account sent for ' + nickname + # NOTE: adding support for '/api/v1/directory seems to create + # federation problems, so avoid implementing that + if path.startswith('/api/v1/blocks'): sendJson = [] sendJsonStr = 'masto API instance blocks sent' diff --git a/media.py b/media.py index dc4ba5346..b7382f4b2 100644 --- a/media.py +++ b/media.py @@ -8,11 +8,13 @@ __status__ = "Production" __module_group__ = "Timeline" import os +import time import datetime import subprocess from random import randint from hashlib import sha1 from auth import createPassword +from utils import getBaseContentFromPost from utils import getFullDomain from utils import getImageExtensions from utils import getVideoExtensions @@ -26,7 +28,8 @@ from shutil import move from city import spoofGeolocation -def replaceYouTube(postJsonObject: {}, replacementDomain: str) -> None: +def replaceYouTube(postJsonObject: {}, replacementDomain: str, + systemLanguage: str) -> None: """Replace YouTube with a replacement domain This denies Google some, but not all, tracking data """ @@ -36,11 +39,13 @@ def replaceYouTube(postJsonObject: {}, replacementDomain: str) -> None: return if not postJsonObject['object'].get('content'): return - if 'www.youtube.com' not in postJsonObject['object']['content']: + contentStr = getBaseContentFromPost(postJsonObject, systemLanguage) + if 'www.youtube.com' not in contentStr: return - postJsonObject['object']['content'] = \ - postJsonObject['object']['content'].replace('www.youtube.com', - replacementDomain) + contentStr = contentStr.replace('www.youtube.com', replacementDomain) + postJsonObject['object']['content'] = contentStr + if postJsonObject['object'].get('contentMap'): + postJsonObject['object']['contentMap'][systemLanguage] = contentStr def _removeMetaData(imageFilename: str, outputFilename: str) -> None: @@ -91,25 +96,64 @@ def _spoofMetaData(baseDir: str, nickname: str, domain: str, camMake, camModel, camSerialNumber) = \ spoofGeolocation(baseDir, spoofCity, currTimeAdjusted, decoySeed, None, None) - os.system('exiftool -artist="' + nickname + '" ' + - '-Make="' + camMake + '" ' + - '-Model="' + camModel + '" ' + - '-Comment="' + str(camSerialNumber) + '" ' + - '-DateTimeOriginal="' + published + '" ' + - '-FileModifyDate="' + published + '" ' + - '-CreateDate="' + published + '" ' + - '-GPSLongitudeRef=' + longitudeRef + ' ' + - '-GPSAltitude=0 ' + - '-GPSLongitude=' + str(longitude) + ' ' + - '-GPSLatitudeRef=' + latitudeRef + ' ' + - '-GPSLatitude=' + str(latitude) + ' ' + - '-Comment="" ' + - outputFilename) # nosec + if os.system('exiftool -artist="' + nickname + '" ' + + '-Make="' + camMake + '" ' + + '-Model="' + camModel + '" ' + + '-Comment="' + str(camSerialNumber) + '" ' + + '-DateTimeOriginal="' + published + '" ' + + '-FileModifyDate="' + published + '" ' + + '-CreateDate="' + published + '" ' + + '-GPSLongitudeRef=' + longitudeRef + ' ' + + '-GPSAltitude=0 ' + + '-GPSLongitude=' + str(longitude) + ' ' + + '-GPSLatitudeRef=' + latitudeRef + ' ' + + '-GPSLatitude=' + str(latitude) + ' ' + + '-Comment="" ' + + outputFilename) != 0: # nosec + print('ERROR: exiftool failed to run') else: print('ERROR: exiftool is not installed') return +def convertImageToLowBandwidth(imageFilename: str) -> None: + """Converts an image to a low bandwidth version + """ + lowBandwidthFilename = imageFilename + '.low' + if os.path.isfile(lowBandwidthFilename): + try: + os.remove(lowBandwidthFilename) + except BaseException: + pass + + cmd = \ + '/usr/bin/convert +noise Multiplicative ' + \ + '-evaluate median 10% -dither Floyd-Steinberg ' + \ + '-monochrome ' + imageFilename + ' ' + lowBandwidthFilename + print('Low bandwidth image conversion: ' + cmd) + subprocess.call(cmd, shell=True) + # wait for conversion to happen + ctr = 0 + while not os.path.isfile(lowBandwidthFilename): + print('Waiting for low bandwidth image conversion ' + str(ctr)) + time.sleep(0.2) + ctr += 1 + if ctr > 100: + print('WARN: timed out waiting for low bandwidth image conversion') + break + if os.path.isfile(lowBandwidthFilename): + try: + os.remove(imageFilename) + except BaseException: + pass + os.rename(lowBandwidthFilename, imageFilename) + if os.path.isfile(imageFilename): + print('Image converted to low bandwidth ' + imageFilename) + else: + print('Low bandwidth converted image not found: ' + + lowBandwidthFilename) + + def processMetaData(baseDir: str, nickname: str, domain: str, imageFilename: str, outputFilename: str, city: str) -> None: @@ -205,7 +249,7 @@ def attachMedia(baseDir: str, httpPrefix: str, nickname: str, domain: str, port: int, postJson: {}, imageFilename: str, mediaType: str, description: str, - city: str) -> {}: + city: str, lowBandwidth: bool) -> {}: """Attaches media to a json object post The description can be None """ @@ -258,6 +302,8 @@ def attachMedia(baseDir: str, httpPrefix: str, if baseDir: if mediaType.startswith('image/'): + if lowBandwidth: + convertImageToLowBandwidth(imageFilename) processMetaData(baseDir, nickname, domain, imageFilename, mediaFilename, city) else: diff --git a/migrate.py b/migrate.py index 2163a1509..a093cc8fe 100644 --- a/migrate.py +++ b/migrate.py @@ -12,6 +12,7 @@ from utils import isAccountDir from utils import getNicknameFromActor from utils import getDomainFromActor from utils import acctDir +from utils import hasGroupType from webfinger import webfingerHandle from blocking import isBlocked from posts import getUserUrl @@ -58,7 +59,7 @@ def _updateMovedHandle(baseDir: str, nickname: str, domain: str, handle = handle[1:] wfRequest = webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - None, __version__, debug) + None, __version__, debug, False) if not wfRequest: print('updateMovedHandle unable to webfinger ' + handle) return ctr @@ -102,13 +103,14 @@ def _updateMovedHandle(baseDir: str, nickname: str, domain: str, if movedToPort: if movedToPort != 80 and movedToPort != 443: movedToDomainFull = movedToDomain + ':' + str(movedToPort) + groupAccount = hasGroupType(baseDir, movedToUrl, None) if isBlocked(baseDir, nickname, domain, movedToNickname, movedToDomain): # someone that you follow has moved to a blocked domain # so just unfollow them unfollowAccount(baseDir, nickname, domain, movedToNickname, movedToDomainFull, - 'following.txt', debug) + debug, groupAccount, 'following.txt') return ctr followingFilename = acctDir(baseDir, nickname, domain) + '/following.txt' @@ -134,7 +136,7 @@ def _updateMovedHandle(baseDir: str, nickname: str, domain: str, unfollowAccount(baseDir, nickname, domain, handleNickname, handleDomain, - 'following.txt', debug) + debug, groupAccount, 'following.txt') ctr += 1 print('Unfollowed ' + handle + ' who has moved to ' + movedToHandle) diff --git a/newsdaemon.py b/newsdaemon.py index 21b01c67a..6e529471b 100644 --- a/newsdaemon.py +++ b/newsdaemon.py @@ -25,6 +25,7 @@ from newswire import getDictFromNewswire from posts import createNewsPost from posts import archivePostsForPerson from content import validHashTag +from utils import getBaseContentFromPost from utils import removeHtml from utils import getFullDomain from utils import loadJson @@ -32,6 +33,7 @@ from utils import saveJson from utils import getStatusNumber from utils import clearFromPostCaches from utils import dangerousMarkup +from utils import localActorUrl from inbox import storeHashTags from session import createSession @@ -279,7 +281,7 @@ def hashtagRuleTree(operators: [], def _hashtagAdd(baseDir: str, httpPrefix: str, domainFull: str, postJsonObject: {}, - actionStr: str, hashtags: []) -> None: + actionStr: str, hashtags: [], systemLanguage: str) -> None: """Adds a hashtag via a hashtag rule """ addHashtag = actionStr.split('add ', 1)[1].strip() @@ -313,7 +315,7 @@ def _hashtagAdd(baseDir: str, httpPrefix: str, domainFull: str, hashtagHtml = \ " #" + htId + "" - content = postJsonObject['object']['content'] + content = getBaseContentFromPost(postJsonObject, systemLanguage) if hashtagHtml in content: return @@ -328,7 +330,7 @@ def _hashtagAdd(baseDir: str, httpPrefix: str, domainFull: str, def _hashtagRemove(httpPrefix: str, domainFull: str, postJsonObject: {}, - actionStr: str, hashtags: []) -> None: + actionStr: str, hashtags: [], systemLanguage: str) -> None: """Removes a hashtag via a hashtag rule """ rmHashtag = actionStr.split('remove ', 1)[1].strip() @@ -343,10 +345,11 @@ def _hashtagRemove(httpPrefix: str, domainFull: str, postJsonObject: {}, hashtagHtml = \ "#" + htId + "" - content = postJsonObject['object']['content'] + content = getBaseContentFromPost(postJsonObject, systemLanguage) if hashtagHtml in content: content = content.replace(hashtagHtml, '').replace(' ', ' ') postJsonObject['object']['content'] = content + postJsonObject['object']['contentMap'][systemLanguage] = content rmTagObject = None for t in postJsonObject['object']['tag']: if t.get('type') and t.get('name'): @@ -365,7 +368,8 @@ def _newswireHashtagProcessing(session, baseDir: str, postJsonObject: {}, cachedWebfingers: {}, federationList: [], sendThreads: [], postLog: [], - moderated: bool, url: str) -> bool: + moderated: bool, url: str, + systemLanguage: str) -> bool: """Applies hashtag rules to a news post. Returns true if the post should be saved to the news timeline of this instance @@ -382,7 +386,7 @@ def _newswireHashtagProcessing(session, baseDir: str, postJsonObject: {}, # get the full text content of the post content = '' if postJsonObject['object'].get('content'): - content += postJsonObject['object']['content'] + content += getBaseContentFromPost(postJsonObject, systemLanguage) if postJsonObject['object'].get('summary'): content += ' ' + postJsonObject['object']['summary'] content = content.lower() @@ -409,11 +413,11 @@ def _newswireHashtagProcessing(session, baseDir: str, postJsonObject: {}, if actionStr.startswith('add '): # add a hashtag _hashtagAdd(baseDir, httpPrefix, domainFull, - postJsonObject, actionStr, hashtags) + postJsonObject, actionStr, hashtags, systemLanguage) elif actionStr.startswith('remove '): # remove a hashtag _hashtagRemove(httpPrefix, domainFull, postJsonObject, - actionStr, hashtags) + actionStr, hashtags, systemLanguage) elif actionStr.startswith('block') or actionStr.startswith('drop'): # Block this item return False @@ -516,7 +520,9 @@ def _convertRSStoActivityPub(baseDir: str, httpPrefix: str, federationList: [], sendThreads: [], postLog: [], maxMirroredArticles: int, - allowLocalNetworkAccess: bool) -> None: + allowLocalNetworkAccess: bool, + systemLanguage: str, + lowBandwidth: bool) -> None: """Converts rss items in a newswire into posts """ if not newswire: @@ -542,8 +548,8 @@ def _convertRSStoActivityPub(baseDir: str, httpPrefix: str, statusNumber, published = getStatusNumber(dateStr) newPostId = \ - httpPrefix + '://' + domain + \ - '/users/news/statuses/' + statusNumber + localActorUrl(httpPrefix, 'news', domain) + \ + '/statuses/' + statusNumber # file where the post is stored filename = basePath + '/' + newPostId.replace('/', '#') + '.json' @@ -590,13 +596,15 @@ def _convertRSStoActivityPub(baseDir: str, httpPrefix: str, mediaType = None imageDescription = None city = 'London, England' + conversationId = None blog = createNewsPost(baseDir, domain, port, httpPrefix, rssDescription, followersOnly, saveToFile, attachImageFilename, mediaType, imageDescription, city, - rssTitle) + rssTitle, systemLanguage, + conversationId, lowBandwidth) if not blog: continue @@ -606,7 +614,7 @@ def _convertRSStoActivityPub(baseDir: str, httpPrefix: str, continue idStr = \ - httpPrefix + '://' + domain + '/users/news' + \ + localActorUrl(httpPrefix, 'news', domain) + \ '/statuses/' + statusNumber + '/replies' blog['news'] = True @@ -626,7 +634,7 @@ def _convertRSStoActivityPub(baseDir: str, httpPrefix: str, blog['object']['published'] = dateStr blog['object']['content'] = rssDescription - blog['object']['contentMap']['en'] = rssDescription + blog['object']['contentMap'][systemLanguage] = rssDescription domainFull = getFullDomain(domain, port) @@ -641,7 +649,7 @@ def _convertRSStoActivityPub(baseDir: str, httpPrefix: str, personCache, cachedWebfingers, federationList, sendThreads, postLog, - moderated, url) + moderated, url, systemLanguage) # save the post and update the index if savePost: @@ -663,7 +671,7 @@ def _convertRSStoActivityPub(baseDir: str, httpPrefix: str, "\" class=\"addedHashtag\" " + \ "rel=\"tag\">#" + \ htId + "" - content = blog['object']['content'] + content = getBaseContentFromPost(blog, systemLanguage) if hashtagHtml not in content: if content.endswith('

'): content = \ @@ -672,6 +680,7 @@ def _convertRSStoActivityPub(baseDir: str, httpPrefix: str, else: content += hashtagHtml blog['object']['content'] = content + blog['object']['contentMap'][systemLanguage] = content # update the newswire tags if new ones have been found by # _newswireHashtagProcessing @@ -748,7 +757,8 @@ def runNewswireDaemon(baseDir: str, httpd, httpd.maxTags, httpd.maxFeedItemSizeKb, httpd.maxNewswirePosts, - httpd.maxCategoriesFeedItemSizeKb) + httpd.maxCategoriesFeedItemSizeKb, + httpd.systemLanguage) if not httpd.newswire: if os.path.isfile(newswireStateFilename): @@ -773,7 +783,9 @@ def runNewswireDaemon(baseDir: str, httpd, httpd.sendThreads, httpd.postLog, httpd.maxMirroredArticles, - httpd.allowLocalNetworkAccess) + httpd.allowLocalNetworkAccess, + httpd.systemLanguage, + httpd.lowBandwidth) print('Newswire feed converted to ActivityPub') if httpd.maxNewsPosts > 0: diff --git a/newswire.py b/newswire.py index 5fba2748e..5f8a1ba0b 100644 --- a/newswire.py +++ b/newswire.py @@ -18,6 +18,7 @@ from datetime import timezone from collections import OrderedDict from utils import validPostDate from categories import setHashtagCategory +from utils import getBaseContentFromPost from utils import hasObjectDict from utils import firstParagraphFromString from utils import isPublicPost @@ -29,6 +30,7 @@ from utils import containsInvalidChars from utils import removeHtml from utils import isAccountDir from utils import acctDir +from utils import localActorUrl from blocking import isBlockedDomain from blocking import isBlockedHashtag from filters import isFiltered @@ -67,8 +69,9 @@ def rss2Header(httpPrefix: str, else: rssStr += \ ' ' + translate[title] + '' + \ - ' ' + httpPrefix + '://' + domainFull + \ - '/users/' + nickname + '/rss.xml' + '' + ' ' + \ + localActorUrl(httpPrefix, nickname, domainFull) + \ + '/rss.xml' + '' return rssStr @@ -290,7 +293,8 @@ def _xml2StrToHashtagCategories(baseDir: str, xmlStr: str, hashtagList = hashtagListStr.split(' ') if not isBlockedHashtag(baseDir, categoryStr): for hashtag in hashtagList: - setHashtagCategory(baseDir, hashtag, categoryStr, force) + setHashtagCategory(baseDir, hashtag, categoryStr, + False, force) def _xml2StrToDict(baseDir: str, domain: str, xmlStr: str, @@ -909,7 +913,7 @@ def _addAccountBlogsToNewswire(baseDir: str, nickname: str, domain: str, newswire: {}, maxBlogsPerAccount: int, indexFilename: str, - maxTags: int) -> None: + maxTags: int, systemLanguage: str) -> None: """Adds blogs for the given account to the newswire """ if not os.path.isfile(indexFilename): @@ -961,7 +965,8 @@ def _addAccountBlogsToNewswire(baseDir: str, nickname: str, domain: str, votes = [] if os.path.isfile(fullPostFilename + '.votes'): votes = loadJson(fullPostFilename + '.votes') - content = postJsonObject['object']['content'] + content = \ + getBaseContentFromPost(postJsonObject, systemLanguage) description = firstParagraphFromString(content) description = removeHtml(description) tagsFromPost = _getHashtagsFromPost(postJsonObject) @@ -981,7 +986,7 @@ def _addAccountBlogsToNewswire(baseDir: str, nickname: str, domain: str, def _addBlogsToNewswire(baseDir: str, domain: str, newswire: {}, maxBlogsPerAccount: int, - maxTags: int) -> None: + maxTags: int, systemLanguage: str) -> None: """Adds blogs from each user account into the newswire """ moderationDict = {} @@ -1009,7 +1014,8 @@ def _addBlogsToNewswire(baseDir: str, domain: str, newswire: {}, domain = handle.split('@')[1] _addAccountBlogsToNewswire(baseDir, nickname, domain, newswire, maxBlogsPerAccount, - blogsIndex, maxTags) + blogsIndex, maxTags, + systemLanguage) break # sort the moderation dict into chronological order, latest first @@ -1029,7 +1035,8 @@ def getDictFromNewswire(session, baseDir: str, domain: str, maxPostsPerSource: int, maxFeedSizeKb: int, maxTags: int, maxFeedItemSizeKb: int, maxNewswirePosts: int, - maxCategoriesFeedItemSizeKb: int) -> {}: + maxCategoriesFeedItemSizeKb: int, + systemLanguage: str) -> {}: """Gets rss feeds as a dictionary from newswire file """ subscriptionsFilename = baseDir + '/accounts/newswire.txt' @@ -1077,7 +1084,7 @@ def getDictFromNewswire(session, baseDir: str, domain: str, # add blogs from each user account _addBlogsToNewswire(baseDir, domain, result, - maxPostsPerSource, maxTags) + maxPostsPerSource, maxTags, systemLanguage) # sort into chronological order, latest first sortedResult = OrderedDict(sorted(result.items(), reverse=True)) diff --git a/ontology/clothesTypes.json b/ontology/clothesTypes.json new file mode 100644 index 000000000..7f0eb517f --- /dev/null +++ b/ontology/clothesTypes.json @@ -0,0 +1,2864 @@ +{ + "@context": { + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "dfc-b": "http://static.datafoodconsortium.org/ontologies/DFC_BusinessOntology.owl#", + "dfc-p": "http://static.datafoodconsortium.org/ontologies/DFC_ProductOntology.owl#", + "dfc-t": "http://static.datafoodconsortium.org/ontologies/DFC_TechnicalOntology.owl#", + "dfc-u": "http://static.datafoodconsortium.org/data/units.rdf#", + "dfc-p:specialize": { + "@type": "@id" + } + }, + "@graph": [ + { + "@id": "https://clothes/data/clothesTypes.rdf#shirt", + "rdfs:label": [ + { + "@value": "La chemise", + "@language": "fr" + }, + { + "@value": "Shirt", + "@language": "de" + }, + { + "@value": "Camisa", + "@language": "es" + }, + { + "@value": "Shirt", + "@language": "en" + }, + { + "@value": "شيرت", + "@language": "ar" + }, + { + "@value": "Shirt", + "@language": "ku" + }, + { + "@value": "Camicia", + "@language": "it" + }, + { + "@value": "Shirt", + "@language": "sw" + }, + { + "@value": "Camisa", + "@language": "pt" + }, + { + "@value": "Shirt", + "@language": "oc" + }, + { + "@value": "рубашка", + "@language": "ru" + }, + { + "@value": "Shirt", + "@language": "cy" + }, + { + "@value": "シャツ", + "@language": "ja" + }, + { + "@value": "An shúchán", + "@language": "ga" + }, + { + "@value": "शर्ट", + "@language": "hi" + }, + { + "@value": "施暴", + "@language": "zh" + }, + { + "@value": "Shirt", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/clothesTypes.rdf#shirt", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#belt", + "rdfs:label": [ + { + "@value": "Ceinture", + "@language": "fr" + }, + { + "@value": "Gürtel", + "@language": "de" + }, + { + "@value": "Cinturón", + "@language": "es" + }, + { + "@value": "Belt", + "@language": "en" + }, + { + "@value": "الحزام", + "@language": "ar" + }, + { + "@value": "Belt", + "@language": "ku" + }, + { + "@value": "Cintura", + "@language": "it" + }, + { + "@value": "Belt", + "@language": "sw" + }, + { + "@value": "Cinto de cinto", + "@language": "pt" + }, + { + "@value": "Belt", + "@language": "oc" + }, + { + "@value": "Пояс", + "@language": "ru" + }, + { + "@value": "Belt", + "@language": "cy" + }, + { + "@value": "ベルト", + "@language": "ja" + }, + { + "@value": "Chreasa", + "@language": "ga" + }, + { + "@value": "बेल्ट", + "@language": "hi" + }, + { + "@value": "B. Belt", + "@language": "zh" + }, + { + "@value": "Belt", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#belt", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#childrens-clothing", + "rdfs:label": [ + { + "@value": "Vêtements pour enfants", + "@language": "fr" + }, + { + "@value": "Kinderkleidung", + "@language": "de" + }, + { + "@value": "Ropa de ninos", + "@language": "es" + }, + { + "@value": "Children's Clothing", + "@language": "en" + }, + { + "@value": "ملابس الأطفال", + "@language": "ar" + }, + { + "@value": "Children's Clothing", + "@language": "ku" + }, + { + "@value": "Abbigliamento per bambini", + "@language": "it" + }, + { + "@value": "Children's Clothing", + "@language": "sw" + }, + { + "@value": "Vestuário infantil", + "@language": "pt" + }, + { + "@value": "Children's Clothing", + "@language": "oc" + }, + { + "@value": "Детская одежда", + "@language": "ru" + }, + { + "@value": "Children's Clothing", + "@language": "cy" + }, + { + "@value": "子供の衣類", + "@language": "ja" + }, + { + "@value": "Éadaí Leanaí", + "@language": "ga" + }, + { + "@value": "बच्चों के वस्त्र", + "@language": "hi" + }, + { + "@value": "儿童入学", + "@language": "zh" + }, + { + "@value": "Children's Clothing", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#childrens-clothing", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#coat", + "rdfs:label": [ + { + "@value": "Manteau", + "@language": "fr" + }, + { + "@value": "Mantel", + "@language": "de" + }, + { + "@value": "Abrigo", + "@language": "es" + }, + { + "@value": "Coat", + "@language": "en" + }, + { + "@value": "Coat", + "@language": "ar" + }, + { + "@value": "Coat", + "@language": "ku" + }, + { + "@value": "Cappotto", + "@language": "it" + }, + { + "@value": "Coat", + "@language": "sw" + }, + { + "@value": "Casa de banho", + "@language": "pt" + }, + { + "@value": "Coat", + "@language": "oc" + }, + { + "@value": "Пальто", + "@language": "ru" + }, + { + "@value": "Coat", + "@language": "cy" + }, + { + "@value": "コーティング", + "@language": "ja" + }, + { + "@value": "Ar Aghaidh Ar Aghaidh", + "@language": "ga" + }, + { + "@value": "कोट", + "@language": "hi" + }, + { + "@value": "A. Coat", + "@language": "zh" + }, + { + "@value": "Coat", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#coat", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#dress", + "rdfs:label": [ + { + "@value": "Robe", + "@language": "fr" + }, + { + "@value": "Kleid", + "@language": "de" + }, + { + "@value": "Vestir", + "@language": "es" + }, + { + "@value": "Dress", + "@language": "en" + }, + { + "@value": "الملابس", + "@language": "ar" + }, + { + "@value": "Dress", + "@language": "ku" + }, + { + "@value": "Abito", + "@language": "it" + }, + { + "@value": "Dress", + "@language": "sw" + }, + { + "@value": "Vestido", + "@language": "pt" + }, + { + "@value": "Dress", + "@language": "oc" + }, + { + "@value": "Платье", + "@language": "ru" + }, + { + "@value": "Dress", + "@language": "cy" + }, + { + "@value": "ドレス", + "@language": "ja" + }, + { + "@value": "Gúna bainise", + "@language": "ga" + }, + { + "@value": "पोशाक", + "@language": "hi" + }, + { + "@value": "博士", + "@language": "zh" + }, + { + "@value": "Dress", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#womens", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#shoes", + "rdfs:label": [ + { + "@value": "Des chaussures", + "@language": "fr" + }, + { + "@value": "Schuhe", + "@language": "de" + }, + { + "@value": "Zapatos", + "@language": "es" + }, + { + "@value": "Shoes", + "@language": "en" + }, + { + "@value": "الأحذية", + "@language": "ar" + }, + { + "@value": "Shoes", + "@language": "ku" + }, + { + "@value": "Scarpe", + "@language": "it" + }, + { + "@value": "Shoes", + "@language": "sw" + }, + { + "@value": "Sapatos", + "@language": "pt" + }, + { + "@value": "Shoes", + "@language": "oc" + }, + { + "@value": "Обувь", + "@language": "ru" + }, + { + "@value": "Shoes", + "@language": "cy" + }, + { + "@value": "シューズ", + "@language": "ja" + }, + { + "@value": "Bróga", + "@language": "ga" + }, + { + "@value": "जूते", + "@language": "hi" + }, + { + "@value": "舒 果", + "@language": "zh" + }, + { + "@value": "Shoes", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#footwear", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#boots", + "rdfs:label": [ + { + "@value": "Bottes", + "@language": "fr" + }, + { + "@value": "Stiefel", + "@language": "de" + }, + { + "@value": "Botas", + "@language": "es" + }, + { + "@value": "Boots", + "@language": "en" + }, + { + "@value": "بوتس", + "@language": "ar" + }, + { + "@value": "Boots", + "@language": "ku" + }, + { + "@value": "Stivali", + "@language": "it" + }, + { + "@value": "Boots", + "@language": "sw" + }, + { + "@value": "Botas", + "@language": "pt" + }, + { + "@value": "Boots", + "@language": "oc" + }, + { + "@value": "Сапоги", + "@language": "ru" + }, + { + "@value": "Boots", + "@language": "cy" + }, + { + "@value": "ブーツ", + "@language": "ja" + }, + { + "@value": "Buataisí Buataisí", + "@language": "ga" + }, + { + "@value": "जूते", + "@language": "hi" + }, + { + "@value": "波 斯", + "@language": "zh" + }, + { + "@value": "Boots", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#footwear", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#gown", + "rdfs:label": [ + { + "@value": "Robe", + "@language": "fr" + }, + { + "@value": "Kleid", + "@language": "de" + }, + { + "@value": "Vestido", + "@language": "es" + }, + { + "@value": "Gown", + "@language": "en" + }, + { + "@value": "فون", + "@language": "ar" + }, + { + "@value": "Gown", + "@language": "ku" + }, + { + "@value": "Abito", + "@language": "it" + }, + { + "@value": "Gown", + "@language": "sw" + }, + { + "@value": "Golpe", + "@language": "pt" + }, + { + "@value": "Gown", + "@language": "oc" + }, + { + "@value": "Ура", + "@language": "ru" + }, + { + "@value": "Gown", + "@language": "cy" + }, + { + "@value": "ガウン", + "@language": "ja" + }, + { + "@value": "Go raibh maith agat", + "@language": "ga" + }, + { + "@value": "गाउन", + "@language": "hi" + }, + { + "@value": "Gown", + "@language": "zh" + }, + { + "@value": "Gown", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#gown", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#hat", + "rdfs:label": [ + { + "@value": "Chapeau", + "@language": "fr" + }, + { + "@value": "Hut", + "@language": "de" + }, + { + "@value": "Sombrero", + "@language": "es" + }, + { + "@value": "Hat", + "@language": "en" + }, + { + "@value": "Hat", + "@language": "ar" + }, + { + "@value": "Hat", + "@language": "ku" + }, + { + "@value": "Cappello", + "@language": "it" + }, + { + "@value": "Hat", + "@language": "sw" + }, + { + "@value": "Chapéu de chapéu", + "@language": "pt" + }, + { + "@value": "Hat", + "@language": "oc" + }, + { + "@value": "Шляпа", + "@language": "ru" + }, + { + "@value": "Hat", + "@language": "cy" + }, + { + "@value": "帽子帽子", + "@language": "ja" + }, + { + "@value": "cineál gas: in airde", + "@language": "ga" + }, + { + "@value": "हैट", + "@language": "hi" + }, + { + "@value": "Hat", + "@language": "zh" + }, + { + "@value": "Hat", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#hat", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#hosiery‎", + "rdfs:label": [ + { + "@value": "Hosiery", + "@language": "fr" + }, + { + "@value": "‎Strumpfwaren", + "@language": "de" + }, + { + "@value": "‎Hosi", + "@language": "es" + }, + { + "@value": "Hosiery‎", + "@language": "en" + }, + { + "@value": "Hosiery", + "@language": "ar" + }, + { + "@value": "Hosiery‎", + "@language": "ku" + }, + { + "@value": "Hosily", + "@language": "it" + }, + { + "@value": "Hosiery‎", + "@language": "sw" + }, + { + "@value": "Hosiery", + "@language": "pt" + }, + { + "@value": "Hosiery‎", + "@language": "oc" + }, + { + "@value": "ГОСЕРИЯ", + "@language": "ru" + }, + { + "@value": "Hosiery‎", + "@language": "cy" + }, + { + "@value": "ホシーリー", + "@language": "ja" + }, + { + "@value": "taiseachas aeir: fliuch", + "@language": "ga" + }, + { + "@value": "होजरी", + "@language": "hi" + }, + { + "@value": "霍西图", + "@language": "zh" + }, + { + "@value": "Hosiery‎", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#hosiery‎", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#jacket", + "rdfs:label": [ + { + "@value": "Veste", + "@language": "fr" + }, + { + "@value": "Jacke", + "@language": "de" + }, + { + "@value": "Chaqueta", + "@language": "es" + }, + { + "@value": "Jacket", + "@language": "en" + }, + { + "@value": "جاكيت", + "@language": "ar" + }, + { + "@value": "Jacket", + "@language": "ku" + }, + { + "@value": "Giacca", + "@language": "it" + }, + { + "@value": "Jacket", + "@language": "sw" + }, + { + "@value": "Casaco", + "@language": "pt" + }, + { + "@value": "Jacket", + "@language": "oc" + }, + { + "@value": "Куртка", + "@language": "ru" + }, + { + "@value": "Jacket", + "@language": "cy" + }, + { + "@value": "ジャケット", + "@language": "ja" + }, + { + "@value": "Jacket céirithe", + "@language": "ga" + }, + { + "@value": "जैकेट", + "@language": "hi" + }, + { + "@value": "Jacket", + "@language": "zh" + }, + { + "@value": "Jacket", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#jacket", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#jeans", + "rdfs:label": [ + { + "@value": "Jeans", + "@language": "fr" + }, + { + "@value": "Jeans", + "@language": "de" + }, + { + "@value": "Vaqueras", + "@language": "es" + }, + { + "@value": "Jeans", + "@language": "en" + }, + { + "@value": "جينز", + "@language": "ar" + }, + { + "@value": "Jeans", + "@language": "ku" + }, + { + "@value": "Jeans", + "@language": "it" + }, + { + "@value": "Jeans", + "@language": "sw" + }, + { + "@value": "Jeans", + "@language": "pt" + }, + { + "@value": "Jeans", + "@language": "oc" + }, + { + "@value": "Джинсы", + "@language": "ru" + }, + { + "@value": "Jeans", + "@language": "cy" + }, + { + "@value": "ジーンズ", + "@language": "ja" + }, + { + "@value": "Amharc ar gach eolas", + "@language": "ga" + }, + { + "@value": "जीन्स", + "@language": "hi" + }, + { + "@value": "Jeans", + "@language": "zh" + }, + { + "@value": "Jeans", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#jeans", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#mask", + "rdfs:label": [ + { + "@value": "Masquer", + "@language": "fr" + }, + { + "@value": "Maske", + "@language": "de" + }, + { + "@value": "Máscara", + "@language": "es" + }, + { + "@value": "Mask", + "@language": "en" + }, + { + "@value": "Mask", + "@language": "ar" + }, + { + "@value": "Mask", + "@language": "ku" + }, + { + "@value": "Maschera", + "@language": "it" + }, + { + "@value": "Mask", + "@language": "sw" + }, + { + "@value": "Máscara", + "@language": "pt" + }, + { + "@value": "Mask", + "@language": "oc" + }, + { + "@value": "Маска", + "@language": "ru" + }, + { + "@value": "Mask", + "@language": "cy" + }, + { + "@value": "マスク", + "@language": "ja" + }, + { + "@value": "An chuid is mó", + "@language": "ga" + }, + { + "@value": "मास्क", + "@language": "hi" + }, + { + "@value": "Mask", + "@language": "zh" + }, + { + "@value": "Mask", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#mask", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#neckwear", + "rdfs:label": [ + { + "@value": "Vêtements de cou", + "@language": "fr" + }, + { + "@value": "Krawatten", + "@language": "de" + }, + { + "@value": "Corbatas", + "@language": "es" + }, + { + "@value": "Neckwear", + "@language": "en" + }, + { + "@value": "Neckwear", + "@language": "ar" + }, + { + "@value": "Neckwear", + "@language": "ku" + }, + { + "@value": "Biancheria da letto", + "@language": "it" + }, + { + "@value": "Neckwear", + "@language": "sw" + }, + { + "@value": "Pescoço", + "@language": "pt" + }, + { + "@value": "Neckwear", + "@language": "oc" + }, + { + "@value": "Обувь", + "@language": "ru" + }, + { + "@value": "Neckwear", + "@language": "cy" + }, + { + "@value": "ネックウェア", + "@language": "ja" + }, + { + "@value": "Seirbhís do Chustaiméirí", + "@language": "ga" + }, + { + "@value": "Neckwear", + "@language": "hi" + }, + { + "@value": "Neckwear", + "@language": "zh" + }, + { + "@value": "Neckwear", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#neckwear", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#scarf", + "rdfs:label": [ + { + "@value": "Écharpe", + "@language": "fr" + }, + { + "@value": "Schal", + "@language": "de" + }, + { + "@value": "Bufanda", + "@language": "es" + }, + { + "@value": "Scarf", + "@language": "en" + }, + { + "@value": "سكارف", + "@language": "ar" + }, + { + "@value": "Scarf", + "@language": "ku" + }, + { + "@value": "Sciarpa", + "@language": "it" + }, + { + "@value": "Scarf", + "@language": "sw" + }, + { + "@value": "Scarf", + "@language": "pt" + }, + { + "@value": "Scarf", + "@language": "oc" + }, + { + "@value": "Шарф", + "@language": "ru" + }, + { + "@value": "Scarf", + "@language": "cy" + }, + { + "@value": "スカーフ", + "@language": "ja" + }, + { + "@value": "riachtanais uisce: measartha", + "@language": "ga" + }, + { + "@value": "दुपट्टा", + "@language": "hi" + }, + { + "@value": "Scarf", + "@language": "zh" + }, + { + "@value": "Scarf", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#neckwear", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#suit", + "rdfs:label": [ + { + "@value": "Costume", + "@language": "fr" + }, + { + "@value": "Anzug", + "@language": "de" + }, + { + "@value": "Traje", + "@language": "es" + }, + { + "@value": "Suit", + "@language": "en" + }, + { + "@value": "بدلة", + "@language": "ar" + }, + { + "@value": "Suit", + "@language": "ku" + }, + { + "@value": "Su", + "@language": "it" + }, + { + "@value": "Suit", + "@language": "sw" + }, + { + "@value": "Terno", + "@language": "pt" + }, + { + "@value": "Suit", + "@language": "oc" + }, + { + "@value": "Подходит", + "@language": "ru" + }, + { + "@value": "Suit", + "@language": "cy" + }, + { + "@value": "スーツ", + "@language": "ja" + }, + { + "@value": "Suímh", + "@language": "ga" + }, + { + "@value": "सूट", + "@language": "hi" + }, + { + "@value": "诉讼", + "@language": "zh" + }, + { + "@value": "Suit", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#suit", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#poncho", + "rdfs:label": [ + { + "@value": "Poncho", + "@language": "fr" + }, + { + "@value": "Poncho", + "@language": "de" + }, + { + "@value": "Poncho", + "@language": "es" + }, + { + "@value": "Poncho", + "@language": "en" + }, + { + "@value": "Poncho", + "@language": "ar" + }, + { + "@value": "Poncho", + "@language": "ku" + }, + { + "@value": "Poncho per bambini", + "@language": "it" + }, + { + "@value": "Poncho", + "@language": "sw" + }, + { + "@value": "Poncho de chuva", + "@language": "pt" + }, + { + "@value": "Poncho", + "@language": "oc" + }, + { + "@value": "Пончо", + "@language": "ru" + }, + { + "@value": "Poncho", + "@language": "cy" + }, + { + "@value": "ポンチョ", + "@language": "ja" + }, + { + "@value": "Roghnaigh gach rud", + "@language": "ga" + }, + { + "@value": "पोंचो", + "@language": "hi" + }, + { + "@value": "Poncho", + "@language": "zh" + }, + { + "@value": "Poncho", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#poncho", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#cloak", + "rdfs:label": [ + { + "@value": "Manteau", + "@language": "fr" + }, + { + "@value": "Mantel", + "@language": "de" + }, + { + "@value": "Capa", + "@language": "es" + }, + { + "@value": "Cloak", + "@language": "en" + }, + { + "@value": "Cloak", + "@language": "ar" + }, + { + "@value": "Cloak", + "@language": "ku" + }, + { + "@value": "Chiusura", + "@language": "it" + }, + { + "@value": "Cloak", + "@language": "sw" + }, + { + "@value": "Cloak", + "@language": "pt" + }, + { + "@value": "Cloak", + "@language": "oc" + }, + { + "@value": "Клоак", + "@language": "ru" + }, + { + "@value": "Cloak", + "@language": "cy" + }, + { + "@value": "ログイン", + "@language": "ja" + }, + { + "@value": "taiseachas aeir: fliuch", + "@language": "ga" + }, + { + "@value": "क्लोक", + "@language": "hi" + }, + { + "@value": "Cloak", + "@language": "zh" + }, + { + "@value": "Cloak", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#cloak", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#sari", + "rdfs:label": [ + { + "@value": "Sari", + "@language": "fr" + }, + { + "@value": "Sari", + "@language": "de" + }, + { + "@value": "Sari", + "@language": "es" + }, + { + "@value": "Sari", + "@language": "en" + }, + { + "@value": "Sari", + "@language": "ar" + }, + { + "@value": "Sari", + "@language": "ku" + }, + { + "@value": "Sari", + "@language": "it" + }, + { + "@value": "Sari", + "@language": "sw" + }, + { + "@value": "Sari", + "@language": "pt" + }, + { + "@value": "Sari", + "@language": "oc" + }, + { + "@value": "Сари", + "@language": "ru" + }, + { + "@value": "Sari", + "@language": "cy" + }, + { + "@value": "サリ", + "@language": "ja" + }, + { + "@value": "An tIarthar", + "@language": "ga" + }, + { + "@value": "साड़ी", + "@language": "hi" + }, + { + "@value": "Sari", + "@language": "zh" + }, + { + "@value": "Sari", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#womens", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#sash", + "rdfs:label": [ + { + "@value": "Ceinture", + "@language": "fr" + }, + { + "@value": "Schärpe", + "@language": "de" + }, + { + "@value": "Faja", + "@language": "es" + }, + { + "@value": "Sash", + "@language": "en" + }, + { + "@value": "Sash", + "@language": "ar" + }, + { + "@value": "Sash", + "@language": "ku" + }, + { + "@value": "Sash.", + "@language": "it" + }, + { + "@value": "Sash", + "@language": "sw" + }, + { + "@value": "Sash", + "@language": "pt" + }, + { + "@value": "Sash", + "@language": "oc" + }, + { + "@value": "Саш", + "@language": "ru" + }, + { + "@value": "Sash", + "@language": "cy" + }, + { + "@value": "サッシュ", + "@language": "ja" + }, + { + "@value": "Saoil", + "@language": "ga" + }, + { + "@value": "साश", + "@language": "hi" + }, + { + "@value": "Sash", + "@language": "zh" + }, + { + "@value": "Sash", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#sash", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#shawl", + "rdfs:label": [ + { + "@value": "Châle", + "@language": "fr" + }, + { + "@value": "Schal", + "@language": "de" + }, + { + "@value": "Chal", + "@language": "es" + }, + { + "@value": "Shawl", + "@language": "en" + }, + { + "@value": "شول", + "@language": "ar" + }, + { + "@value": "Shawl", + "@language": "ku" + }, + { + "@value": "Shaw", + "@language": "it" + }, + { + "@value": "Shawl", + "@language": "sw" + }, + { + "@value": "Shawl.", + "@language": "pt" + }, + { + "@value": "Shawl", + "@language": "oc" + }, + { + "@value": "Шаль", + "@language": "ru" + }, + { + "@value": "Shawl", + "@language": "cy" + }, + { + "@value": "ショール", + "@language": "ja" + }, + { + "@value": "cineál gas: in airde", + "@language": "ga" + }, + { + "@value": "शॉल", + "@language": "hi" + }, + { + "@value": "Shawl", + "@language": "zh" + }, + { + "@value": "Shawl", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#womens", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#skirt", + "rdfs:label": [ + { + "@value": "Jupe", + "@language": "fr" + }, + { + "@value": "Rock", + "@language": "de" + }, + { + "@value": "Falda", + "@language": "es" + }, + { + "@value": "Skirt", + "@language": "en" + }, + { + "@value": "Skirt", + "@language": "ar" + }, + { + "@value": "Skirt", + "@language": "ku" + }, + { + "@value": "Gomma", + "@language": "it" + }, + { + "@value": "Skirt", + "@language": "sw" + }, + { + "@value": "Saia!", + "@language": "pt" + }, + { + "@value": "Skirt", + "@language": "oc" + }, + { + "@value": "Юбка", + "@language": "ru" + }, + { + "@value": "Skirt", + "@language": "cy" + }, + { + "@value": "スカート", + "@language": "ja" + }, + { + "@value": "Scátála", + "@language": "ga" + }, + { + "@value": "स्कर्ट", + "@language": "hi" + }, + { + "@value": "施 士", + "@language": "zh" + }, + { + "@value": "Skirt", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#womens", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#trousers", + "rdfs:label": [ + { + "@value": "Pantalon", + "@language": "fr" + }, + { + "@value": "Hose", + "@language": "de" + }, + { + "@value": "Pantalones", + "@language": "es" + }, + { + "@value": "Trousers", + "@language": "en" + }, + { + "@value": "البنادق", + "@language": "ar" + }, + { + "@value": "Trousers", + "@language": "ku" + }, + { + "@value": "Pantaloni", + "@language": "it" + }, + { + "@value": "Trousers", + "@language": "sw" + }, + { + "@value": "Calças", + "@language": "pt" + }, + { + "@value": "Trousers", + "@language": "oc" + }, + { + "@value": "Брюки", + "@language": "ru" + }, + { + "@value": "Trousers", + "@language": "cy" + }, + { + "@value": "Trousersの使い方", + "@language": "ja" + }, + { + "@value": "riachtanais uisce: measartha", + "@language": "ga" + }, + { + "@value": "पतलून", + "@language": "hi" + }, + { + "@value": "导 言", + "@language": "zh" + }, + { + "@value": "Trousers", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#trousers", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#shorts", + "rdfs:label": [ + { + "@value": "Shorts", + "@language": "fr" + }, + { + "@value": "Kurze Hose", + "@language": "de" + }, + { + "@value": "Bermudas", + "@language": "es" + }, + { + "@value": "Shorts", + "@language": "en" + }, + { + "@value": "قصيرة", + "@language": "ar" + }, + { + "@value": "Shorts", + "@language": "ku" + }, + { + "@value": "Pantaloni", + "@language": "it" + }, + { + "@value": "Shorts", + "@language": "sw" + }, + { + "@value": "Calções", + "@language": "pt" + }, + { + "@value": "Shorts", + "@language": "oc" + }, + { + "@value": "Шорты", + "@language": "ru" + }, + { + "@value": "Shorts", + "@language": "cy" + }, + { + "@value": "ショートパンツ", + "@language": "ja" + }, + { + "@value": "Deontais do Mhic Léinn", + "@language": "ga" + }, + { + "@value": "शॉर्ट", + "@language": "hi" + }, + { + "@value": "短期", + "@language": "zh" + }, + { + "@value": "Shorts", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#shorts", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#underwear", + "rdfs:label": [ + { + "@value": "Sous-vêtement", + "@language": "fr" + }, + { + "@value": "Unterwäsche", + "@language": "de" + }, + { + "@value": "Ropa interior", + "@language": "es" + }, + { + "@value": "Underwear", + "@language": "en" + }, + { + "@value": "الملابس الداخلية", + "@language": "ar" + }, + { + "@value": "Underwear", + "@language": "ku" + }, + { + "@value": "Biancheria intima", + "@language": "it" + }, + { + "@value": "Underwear", + "@language": "sw" + }, + { + "@value": "Roupa interior", + "@language": "pt" + }, + { + "@value": "Underwear", + "@language": "oc" + }, + { + "@value": "Нижнее белье", + "@language": "ru" + }, + { + "@value": "Underwear", + "@language": "cy" + }, + { + "@value": "アンダーウェア", + "@language": "ja" + }, + { + "@value": "Fo-éadaí", + "@language": "ga" + }, + { + "@value": "अंडरवियर", + "@language": "hi" + }, + { + "@value": "在韦尔", + "@language": "zh" + }, + { + "@value": "Underwear", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#underwear", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#socks", + "rdfs:label": [ + { + "@value": "Des chaussettes", + "@language": "fr" + }, + { + "@value": "Socken", + "@language": "de" + }, + { + "@value": "Medias", + "@language": "es" + }, + { + "@value": "Socks", + "@language": "en" + }, + { + "@value": "جوارب", + "@language": "ar" + }, + { + "@value": "Socks", + "@language": "ku" + }, + { + "@value": "Calzini", + "@language": "it" + }, + { + "@value": "Socks", + "@language": "sw" + }, + { + "@value": "Meias", + "@language": "pt" + }, + { + "@value": "Socks", + "@language": "oc" + }, + { + "@value": "Носки", + "@language": "ru" + }, + { + "@value": "Socks", + "@language": "cy" + }, + { + "@value": "靴下", + "@language": "ja" + }, + { + "@value": "Síológa", + "@language": "ga" + }, + { + "@value": "मोज़े", + "@language": "hi" + }, + { + "@value": "袭击", + "@language": "zh" + }, + { + "@value": "Socks", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#footwear", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#helmet", + "rdfs:label": [ + { + "@value": "Casque", + "@language": "fr" + }, + { + "@value": "Helm", + "@language": "de" + }, + { + "@value": "Casco", + "@language": "es" + }, + { + "@value": "Helmet", + "@language": "en" + }, + { + "@value": "الخوذة", + "@language": "ar" + }, + { + "@value": "Helmet", + "@language": "ku" + }, + { + "@value": "Casco", + "@language": "it" + }, + { + "@value": "Helmet", + "@language": "sw" + }, + { + "@value": "Capacete", + "@language": "pt" + }, + { + "@value": "Helmet", + "@language": "oc" + }, + { + "@value": "Шлем", + "@language": "ru" + }, + { + "@value": "Helmet", + "@language": "cy" + }, + { + "@value": "ヘルメット", + "@language": "ja" + }, + { + "@value": "taiseachas aeir: fliuch", + "@language": "ga" + }, + { + "@value": "हेलमेट", + "@language": "hi" + }, + { + "@value": "赫尔特·赫曼特", + "@language": "zh" + }, + { + "@value": "Helmet", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#helmet", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#gloves", + "rdfs:label": [ + { + "@value": "Gants", + "@language": "fr" + }, + { + "@value": "Handschuhe", + "@language": "de" + }, + { + "@value": "Guantes", + "@language": "es" + }, + { + "@value": "Gloves", + "@language": "en" + }, + { + "@value": "Gloves", + "@language": "ar" + }, + { + "@value": "Gloves", + "@language": "ku" + }, + { + "@value": "Guanti", + "@language": "it" + }, + { + "@value": "Gloves", + "@language": "sw" + }, + { + "@value": "Luvas", + "@language": "pt" + }, + { + "@value": "Gloves", + "@language": "oc" + }, + { + "@value": "Перчатки", + "@language": "ru" + }, + { + "@value": "Gloves", + "@language": "cy" + }, + { + "@value": "グローブ", + "@language": "ja" + }, + { + "@value": "riachtanais uisce: measartha", + "@language": "ga" + }, + { + "@value": "दस्ताने", + "@language": "hi" + }, + { + "@value": "Gloves", + "@language": "zh" + }, + { + "@value": "Gloves", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#gloves", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#kurta", + "rdfs:label": [ + { + "@value": "Kurta", + "@language": "fr" + }, + { + "@value": "Kurta", + "@language": "de" + }, + { + "@value": "Kurta", + "@language": "es" + }, + { + "@value": "Kurta", + "@language": "en" + }, + { + "@value": "كورتا", + "@language": "ar" + }, + { + "@value": "Kurta", + "@language": "ku" + }, + { + "@value": "Kurta", + "@language": "it" + }, + { + "@value": "Kurta", + "@language": "sw" + }, + { + "@value": "Kurta", + "@language": "pt" + }, + { + "@value": "Kurta", + "@language": "oc" + }, + { + "@value": "Курта", + "@language": "ru" + }, + { + "@value": "Kurta", + "@language": "cy" + }, + { + "@value": "カルタ", + "@language": "ja" + }, + { + "@value": "An Chóiré Theas", + "@language": "ga" + }, + { + "@value": "कुर्त", + "@language": "hi" + }, + { + "@value": "Kurta", + "@language": "zh" + }, + { + "@value": "Kurta", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#kurta", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#sherwani", + "rdfs:label": [ + { + "@value": "Sherwani", + "@language": "fr" + }, + { + "@value": "Sherwani", + "@language": "de" + }, + { + "@value": "Sherwani", + "@language": "es" + }, + { + "@value": "Sherwani", + "@language": "en" + }, + { + "@value": "Sherwani", + "@language": "ar" + }, + { + "@value": "Sherwani", + "@language": "ku" + }, + { + "@value": "Sherwani", + "@language": "it" + }, + { + "@value": "Sherwani", + "@language": "sw" + }, + { + "@value": "Sherwani", + "@language": "pt" + }, + { + "@value": "Sherwani", + "@language": "oc" + }, + { + "@value": "Шервани", + "@language": "ru" + }, + { + "@value": "Sherwani", + "@language": "cy" + }, + { + "@value": "シャーワニ", + "@language": "ja" + }, + { + "@value": "Seirbhís do Chustaiméirí", + "@language": "ga" + }, + { + "@value": "शेरवानी", + "@language": "hi" + }, + { + "@value": "Sherwani", + "@language": "zh" + }, + { + "@value": "Sherwani", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#mens", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#shalwar-kameez", + "rdfs:label": [ + { + "@value": "Shalwar Kameez", + "@language": "fr" + }, + { + "@value": "Shalwar Kameez", + "@language": "de" + }, + { + "@value": "Shalwar Kameez", + "@language": "es" + }, + { + "@value": "Shalwar Kameez", + "@language": "en" + }, + { + "@value": "Shalwar Kameez", + "@language": "ar" + }, + { + "@value": "Shalwar Kameez", + "@language": "ku" + }, + { + "@value": "Shalwar Kameez", + "@language": "it" + }, + { + "@value": "Shalwar Kameez", + "@language": "sw" + }, + { + "@value": "Shalwar Kameez", + "@language": "pt" + }, + { + "@value": "Shalwar Kameez", + "@language": "oc" + }, + { + "@value": "Шальвар Камеез", + "@language": "ru" + }, + { + "@value": "Shalwar Kameez", + "@language": "cy" + }, + { + "@value": "シャーラー・カメエズ", + "@language": "ja" + }, + { + "@value": "cliceáil grianghraf a mhéadú", + "@language": "ga" + }, + { + "@value": "शालवार कमीज", + "@language": "hi" + }, + { + "@value": "Shalwar Kameez", + "@language": "zh" + }, + { + "@value": "Shalwar Kameez", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#womens", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#cheongsam", + "rdfs:label": [ + { + "@value": "Cheongsam", + "@language": "fr" + }, + { + "@value": "Cheongsam", + "@language": "de" + }, + { + "@value": "Cheongsam", + "@language": "es" + }, + { + "@value": "Cheongsam", + "@language": "en" + }, + { + "@value": "Cheongsam", + "@language": "ar" + }, + { + "@value": "Cheongsam", + "@language": "ku" + }, + { + "@value": "Cheongsam", + "@language": "it" + }, + { + "@value": "Cheongsam", + "@language": "sw" + }, + { + "@value": "Cheongsam", + "@language": "pt" + }, + { + "@value": "Cheongsam", + "@language": "oc" + }, + { + "@value": "Чеонгсам", + "@language": "ru" + }, + { + "@value": "Cheongsam", + "@language": "cy" + }, + { + "@value": "チェオンサム", + "@language": "ja" + }, + { + "@value": "An bhfuil a fhios agat?", + "@language": "ga" + }, + { + "@value": "चेंग्साम", + "@language": "hi" + }, + { + "@value": "Cheongsam", + "@language": "zh" + }, + { + "@value": "Cheongsam", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#womens", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#áo-bà-ba", + "rdfs:label": [ + { + "@value": "Áo bà ba", + "@language": "fr" + }, + { + "@value": "Áo bà ba", + "@language": "de" + }, + { + "@value": "Áo bà ba", + "@language": "es" + }, + { + "@value": "Áo bà ba", + "@language": "en" + }, + { + "@value": "Áo bà ba", + "@language": "ar" + }, + { + "@value": "Áo bà ba", + "@language": "ku" + }, + { + "@value": "Áo bà ba", + "@language": "it" + }, + { + "@value": "Áo bà ba", + "@language": "sw" + }, + { + "@value": "Áu bà ba", + "@language": "pt" + }, + { + "@value": "Áo bà ba", + "@language": "oc" + }, + { + "@value": "Áo bà бa", + "@language": "ru" + }, + { + "@value": "Áo bà ba", + "@language": "cy" + }, + { + "@value": "アオバババ", + "@language": "ja" + }, + { + "@value": "Bhí an t-eolas mícheart nó as dáta", + "@language": "ga" + }, + { + "@value": "ao bà ba", + "@language": "hi" + }, + { + "@value": "阿尔巴", + "@language": "zh" + }, + { + "@value": "Áo bà ba", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#áo-bà-ba", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#áo-dài", + "rdfs:label": [ + { + "@value": "Áo dài", + "@language": "fr" + }, + { + "@value": "Áo dài", + "@language": "de" + }, + { + "@value": "Áo dài", + "@language": "es" + }, + { + "@value": "Áo dài", + "@language": "en" + }, + { + "@value": "Áo dài", + "@language": "ar" + }, + { + "@value": "Áo dài", + "@language": "ku" + }, + { + "@value": "Áo dài", + "@language": "it" + }, + { + "@value": "Áo dài", + "@language": "sw" + }, + { + "@value": "ÁFRICA", + "@language": "pt" + }, + { + "@value": "Áo dài", + "@language": "oc" + }, + { + "@value": "Ао dài", + "@language": "ru" + }, + { + "@value": "Áo dài", + "@language": "cy" + }, + { + "@value": "アオ・ダイ", + "@language": "ja" + }, + { + "@value": "Déan teagmháil linn", + "@language": "ga" + }, + { + "@value": "ao dài", + "@language": "hi" + }, + { + "@value": "阿尔托·阿比", + "@language": "zh" + }, + { + "@value": "Áo dài", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#áo-dài", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#halter-top", + "rdfs:label": [ + { + "@value": "Halter haut", + "@language": "fr" + }, + { + "@value": "Bikini-Oberteil", + "@language": "de" + }, + { + "@value": "Camiseta sin mangas", + "@language": "es" + }, + { + "@value": "Halter top", + "@language": "en" + }, + { + "@value": "قمة الهضاب", + "@language": "ar" + }, + { + "@value": "Halter top", + "@language": "ku" + }, + { + "@value": "Top Halter", + "@language": "it" + }, + { + "@value": "Halter top", + "@language": "sw" + }, + { + "@value": "Top de Halter", + "@language": "pt" + }, + { + "@value": "Halter top", + "@language": "oc" + }, + { + "@value": "Halter топ", + "@language": "ru" + }, + { + "@value": "Halter top", + "@language": "cy" + }, + { + "@value": "ハルタートップ", + "@language": "ja" + }, + { + "@value": "barr Halter", + "@language": "ga" + }, + { + "@value": "Halter शीर्ष", + "@language": "hi" + }, + { + "@value": "哈龙", + "@language": "zh" + }, + { + "@value": "Halter top", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#womens", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#sandals", + "rdfs:label": [ + { + "@value": "Des sandales", + "@language": "fr" + }, + { + "@value": "Sandalen", + "@language": "de" + }, + { + "@value": "Sandalias", + "@language": "es" + }, + { + "@value": "Sandals", + "@language": "en" + }, + { + "@value": "الرمال", + "@language": "ar" + }, + { + "@value": "Sandals", + "@language": "ku" + }, + { + "@value": "Sandali Sandali", + "@language": "it" + }, + { + "@value": "Sandals", + "@language": "sw" + }, + { + "@value": "Sandálias de sandálias", + "@language": "pt" + }, + { + "@value": "Sandals", + "@language": "oc" + }, + { + "@value": "Сандалии", + "@language": "ru" + }, + { + "@value": "Sandals", + "@language": "cy" + }, + { + "@value": "サンダル", + "@language": "ja" + }, + { + "@value": "Sandals", + "@language": "ga" + }, + { + "@value": "सैंडल", + "@language": "hi" + }, + { + "@value": "圣城", + "@language": "zh" + }, + { + "@value": "Sandals", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#footwear", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#slippers", + "rdfs:label": [ + { + "@value": "Chaussons", + "@language": "fr" + }, + { + "@value": "Hausschuhe", + "@language": "de" + }, + { + "@value": "Zapatillas", + "@language": "es" + }, + { + "@value": "Slippers", + "@language": "en" + }, + { + "@value": "مشبك", + "@language": "ar" + }, + { + "@value": "Slippers", + "@language": "ku" + }, + { + "@value": "pantofole pantofole", + "@language": "it" + }, + { + "@value": "Slippers", + "@language": "sw" + }, + { + "@value": "Chinelos", + "@language": "pt" + }, + { + "@value": "Slippers", + "@language": "oc" + }, + { + "@value": "Тапочки", + "@language": "ru" + }, + { + "@value": "Slippers", + "@language": "cy" + }, + { + "@value": "スリッパ", + "@language": "ja" + }, + { + "@value": "Seirbhís do Chustaiméirí", + "@language": "ga" + }, + { + "@value": "चप्पल", + "@language": "hi" + }, + { + "@value": "速 度", + "@language": "zh" + }, + { + "@value": "Slippers", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#footwear", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://clothes/data/clothesTypes.rdf#kilt", + "rdfs:label": [ + { + "@value": "Kilt", + "@language": "en" + }, + { + "@value": "Kilt", + "@language": "ar" + }, + { + "@value": "Kilt", + "@language": "ku" + }, + { + "@value": "Kilt", + "@language": "es" + }, + { + "@value": "Chilometraggio", + "@language": "it" + }, + { + "@value": "Kilt", + "@language": "de" + }, + { + "@value": "Kilt", + "@language": "sw" + }, + { + "@value": "Fornos industriais", + "@language": "pt" + }, + { + "@value": "Kilt", + "@language": "oc" + }, + { + "@value": "Килт", + "@language": "ru" + }, + { + "@value": "Kilt", + "@language": "cy" + }, + { + "@value": "キルト", + "@language": "ja" + }, + { + "@value": "Inis dúinn, le do thoil...", + "@language": "ga" + }, + { + "@value": "किल", + "@language": "hi" + }, + { + "@value": "杀害", + "@language": "zh" + }, + { + "@value": "Kilt", + "@language": "fr" + }, + { + "@value": "Kilt", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://clothes/data/toolTypes.rdf#kilt", + "@type": "dfc-p:ProductType" + } + ] +} diff --git a/ontology/foodTypes.json b/ontology/foodTypes.json new file mode 100644 index 000000000..75eb0d6ae --- /dev/null +++ b/ontology/foodTypes.json @@ -0,0 +1,18389 @@ +{ + "@context": { + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "dfc-b": "http://static.datafoodconsortium.org/ontologies/DFC_BusinessOntology.owl#", + "dfc-p": "http://static.datafoodconsortium.org/ontologies/DFC_ProductOntology.owl#", + "dfc-t": "http://static.datafoodconsortium.org/ontologies/DFC_TechnicalOntology.owl#", + "dfc-u": "http://static.datafoodconsortium.org/data/units.rdf#", + "dfc-p:specialize": { + "@type": "@id" + } + }, + "@graph": [ + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#soft-drink", + "rdfs:label": [ + { + "@value": "Boisson non alcoolisée", + "@language": "fr" + }, + { + "@value": "Soft drink", + "@language": "de" + }, + { + "@value": "Soft drink", + "@language": "es" + }, + { + "@value": "Soft drink", + "@language": "en" + }, + { + "@value": "مشروب مُتعدّد", + "@language": "ar" + }, + { + "@value": "Soft drink", + "@language": "ku" + }, + { + "@value": "Bevanda morbida", + "@language": "it" + }, + { + "@value": "Soft drink", + "@language": "sw" + }, + { + "@value": "Bebida macia", + "@language": "pt" + }, + { + "@value": "Soft drink", + "@language": "oc" + }, + { + "@value": "Безалкогольный напиток", + "@language": "ru" + }, + { + "@value": "Soft drink", + "@language": "cy" + }, + { + "@value": "ソフトドリンク", + "@language": "ja" + }, + { + "@value": "Deoch bog", + "@language": "ga" + }, + { + "@value": "शीतल पेय", + "@language": "hi" + }, + { + "@value": "软饮料", + "@language": "zh" + }, + { + "@value": "Soft drink", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#drink", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#salad", + "rdfs:label": [ + { + "@value": "Salade", + "@language": "fr" + }, + { + "@value": "Salad", + "@language": "de" + }, + { + "@value": "Salad", + "@language": "es" + }, + { + "@value": "Salad", + "@language": "en" + }, + { + "@value": "سلام", + "@language": "ar" + }, + { + "@value": "Salad", + "@language": "ku" + }, + { + "@value": "Salato", + "@language": "it" + }, + { + "@value": "Salad", + "@language": "sw" + }, + { + "@value": "Salada", + "@language": "pt" + }, + { + "@value": "Salad", + "@language": "oc" + }, + { + "@value": "Салат", + "@language": "ru" + }, + { + "@value": "Salad", + "@language": "cy" + }, + { + "@value": "サラダ", + "@language": "ja" + }, + { + "@value": "An tIarthar", + "@language": "ga" + }, + { + "@value": "सलाद", + "@language": "hi" + }, + { + "@value": "萨利德", + "@language": "zh" + }, + { + "@value": "Salad", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#egg", + "rdfs:label": [ + { + "@value": "Oeuf", + "@language": "fr" + }, + { + "@value": "Egg", + "@language": "de" + }, + { + "@value": "Egg", + "@language": "es" + }, + { + "@value": "Egg", + "@language": "en" + }, + { + "@value": "البيض", + "@language": "ar" + }, + { + "@value": "Egg", + "@language": "ku" + }, + { + "@value": "Uova", + "@language": "it" + }, + { + "@value": "Egg", + "@language": "sw" + }, + { + "@value": "Ovo", + "@language": "pt" + }, + { + "@value": "Egg", + "@language": "oc" + }, + { + "@value": "Яйцо", + "@language": "ru" + }, + { + "@value": "Egg", + "@language": "cy" + }, + { + "@value": "エッグ", + "@language": "ja" + }, + { + "@value": "Uibheacha", + "@language": "ga" + }, + { + "@value": "अंडा", + "@language": "hi" + }, + { + "@value": "加重", + "@language": "zh" + }, + { + "@value": "Egg", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#meat-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#old-variety-squash", + "rdfs:label": [ + { + "@value": "Variété ancienne", + "@language": "fr" + }, + { + "@value": "Old variety squash", + "@language": "de" + }, + { + "@value": "Old variety squash", + "@language": "es" + }, + { + "@value": "Old variety squash", + "@language": "en" + }, + { + "@value": "سكواش قديم", + "@language": "ar" + }, + { + "@value": "Old variety squash", + "@language": "ku" + }, + { + "@value": "Vecchia varietà squash", + "@language": "it" + }, + { + "@value": "Old variety squash", + "@language": "sw" + }, + { + "@value": "Squash variedade antiga", + "@language": "pt" + }, + { + "@value": "Old variety squash", + "@language": "oc" + }, + { + "@value": "Старый сорт squash", + "@language": "ru" + }, + { + "@value": "Old variety squash", + "@language": "cy" + }, + { + "@value": "古い品種スカッシュ", + "@language": "ja" + }, + { + "@value": "Scuais d'éagsúlacht", + "@language": "ga" + }, + { + "@value": "पुरानी विविधता स्क्वैश", + "@language": "hi" + }, + { + "@value": "老百姓", + "@language": "zh" + }, + { + "@value": "Old variety squash", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#squash", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#fig", + "rdfs:label": [ + { + "@value": "Figue", + "@language": "fr" + }, + { + "@value": "Fig", + "@language": "de" + }, + { + "@value": "Fig", + "@language": "es" + }, + { + "@value": "Fig", + "@language": "en" + }, + { + "@value": "Fig", + "@language": "ar" + }, + { + "@value": "Fig", + "@language": "ku" + }, + { + "@value": "Fig.", + "@language": "it" + }, + { + "@value": "Fig", + "@language": "sw" + }, + { + "@value": "Fig.", + "@language": "pt" + }, + { + "@value": "Fig", + "@language": "oc" + }, + { + "@value": "Рис", + "@language": "ru" + }, + { + "@value": "Fig", + "@language": "cy" + }, + { + "@value": "フィクション", + "@language": "ja" + }, + { + "@value": "Figiúr an Fhiachais", + "@language": "ga" + }, + { + "@value": "अंजीर", + "@language": "hi" + }, + { + "@value": "概览", + "@language": "zh" + }, + { + "@value": "Fig", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#fruit", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#rocket", + "rdfs:label": [ + { + "@value": "Roquette", + "@language": "fr" + }, + { + "@value": "Rocket", + "@language": "de" + }, + { + "@value": "Rocket", + "@language": "es" + }, + { + "@value": "Rocket", + "@language": "en" + }, + { + "@value": "Rocket", + "@language": "ar" + }, + { + "@value": "Rocket", + "@language": "ku" + }, + { + "@value": "Rocket", + "@language": "it" + }, + { + "@value": "Rocket", + "@language": "sw" + }, + { + "@value": "Rocket", + "@language": "pt" + }, + { + "@value": "Rocket", + "@language": "oc" + }, + { + "@value": "Ракет", + "@language": "ru" + }, + { + "@value": "Rocket", + "@language": "cy" + }, + { + "@value": "ロケット", + "@language": "ja" + }, + { + "@value": "riachtanais uisce: measartha", + "@language": "ga" + }, + { + "@value": "रॉकेट", + "@language": "hi" + }, + { + "@value": "卢旺达", + "@language": "zh" + }, + { + "@value": "Rocket", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#salad", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#beef", + "rdfs:label": [ + { + "@value": "Viande bovine", + "@language": "fr" + }, + { + "@value": "Beef", + "@language": "de" + }, + { + "@value": "Beef", + "@language": "es" + }, + { + "@value": "Beef", + "@language": "en" + }, + { + "@value": "لحم البقر", + "@language": "ar" + }, + { + "@value": "Beef", + "@language": "ku" + }, + { + "@value": "Beef", + "@language": "it" + }, + { + "@value": "Beef", + "@language": "sw" + }, + { + "@value": "Carne de bovino", + "@language": "pt" + }, + { + "@value": "Beef", + "@language": "oc" + }, + { + "@value": "Говядина", + "@language": "ru" + }, + { + "@value": "Beef", + "@language": "cy" + }, + { + "@value": "牛丼", + "@language": "ja" + }, + { + "@value": "Fúinn", + "@language": "ga" + }, + { + "@value": "बीफ", + "@language": "hi" + }, + { + "@value": "标题", + "@language": "zh" + }, + { + "@value": "Beef", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#meat-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#sheep-mature-cheese", + "rdfs:label": [ + { + "@value": "Fromage affinés", + "@language": "fr" + }, + { + "@value": "Sheep Mature cheese", + "@language": "de" + }, + { + "@value": "Sheep Mature cheese", + "@language": "es" + }, + { + "@value": "Sheep Mature cheese", + "@language": "en" + }, + { + "@value": "جبنة شيب", + "@language": "ar" + }, + { + "@value": "Sheep Mature cheese", + "@language": "ku" + }, + { + "@value": "Pecora Formaggi stagionati", + "@language": "it" + }, + { + "@value": "Sheep Mature cheese", + "@language": "sw" + }, + { + "@value": "Queijo de carneiro", + "@language": "pt" + }, + { + "@value": "Sheep Mature cheese", + "@language": "oc" + }, + { + "@value": "Овцы Зрелые сыры", + "@language": "ru" + }, + { + "@value": "Sheep Mature cheese", + "@language": "cy" + }, + { + "@value": "Sheep 成熟した チーズ", + "@language": "ja" + }, + { + "@value": "Cáis lánfhásta", + "@language": "ga" + }, + { + "@value": "भेड़ परिपक्व पनीर", + "@language": "hi" + }, + { + "@value": "圣诞生", + "@language": "zh" + }, + { + "@value": "Sheep Mature cheese", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#sheep-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#sheep-milk", + "rdfs:label": [ + { + "@value": "Lait", + "@language": "fr" + }, + { + "@value": "Sheep Milk", + "@language": "de" + }, + { + "@value": "Sheep Milk", + "@language": "es" + }, + { + "@value": "Sheep Milk", + "@language": "en" + }, + { + "@value": "حليب الخراف", + "@language": "ar" + }, + { + "@value": "Sheep Milk", + "@language": "ku" + }, + { + "@value": "Latte di pecora", + "@language": "it" + }, + { + "@value": "Sheep Milk", + "@language": "sw" + }, + { + "@value": "Leite de ovelhas", + "@language": "pt" + }, + { + "@value": "Sheep Milk", + "@language": "oc" + }, + { + "@value": "Овцы Молоко", + "@language": "ru" + }, + { + "@value": "Sheep Milk", + "@language": "cy" + }, + { + "@value": "羊ミルク", + "@language": "ja" + }, + { + "@value": "Uisce agus Séarachas", + "@language": "ga" + }, + { + "@value": "भेड़ दूध", + "@language": "hi" + }, + { + "@value": "她", + "@language": "zh" + }, + { + "@value": "Sheep Milk", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#sheep-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#chanterelle-mushroom", + "rdfs:label": [ + { + "@value": "Chanterelle", + "@language": "fr" + }, + { + "@value": "Chanterelle mushroom", + "@language": "de" + }, + { + "@value": "Chanterelle mushroom", + "@language": "es" + }, + { + "@value": "Chanterelle mushroom", + "@language": "en" + }, + { + "@value": "Chanterelle mushroom", + "@language": "ar" + }, + { + "@value": "Chanterelle mushroom", + "@language": "ku" + }, + { + "@value": "Fungo di Chanterelle", + "@language": "it" + }, + { + "@value": "Chanterelle mushroom", + "@language": "sw" + }, + { + "@value": "Cogumelo Chanterelle", + "@language": "pt" + }, + { + "@value": "Chanterelle mushroom", + "@language": "oc" + }, + { + "@value": "Гриб", + "@language": "ru" + }, + { + "@value": "Chanterelle mushroom", + "@language": "cy" + }, + { + "@value": "シャンテルレのマッシュルーム", + "@language": "ja" + }, + { + "@value": "Muisiriún Chanterelle", + "@language": "ga" + }, + { + "@value": "Chanterelle मशरूम", + "@language": "hi" + }, + { + "@value": "Chanterelle Milshroom", + "@language": "zh" + }, + { + "@value": "Chanterelle mushroom", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#mushroom", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#goat-sweet-yogurt", + "rdfs:label": [ + { + "@value": "Yaourt sucré", + "@language": "fr" + }, + { + "@value": "Goat sweet yogurt", + "@language": "de" + }, + { + "@value": "Goat sweet yogurt", + "@language": "es" + }, + { + "@value": "Goat sweet yogurt", + "@language": "en" + }, + { + "@value": "زبادي حلو", + "@language": "ar" + }, + { + "@value": "Goat sweet yogurt", + "@language": "ku" + }, + { + "@value": "Goat dolce yogurt", + "@language": "it" + }, + { + "@value": "Goat sweet yogurt", + "@language": "sw" + }, + { + "@value": "Iogurte doce de cabra", + "@language": "pt" + }, + { + "@value": "Goat sweet yogurt", + "@language": "oc" + }, + { + "@value": "Говядина сладкая йогурт", + "@language": "ru" + }, + { + "@value": "Goat sweet yogurt", + "@language": "cy" + }, + { + "@value": "ゴート甘いヨーグルト", + "@language": "ja" + }, + { + "@value": "Goat iógart milis", + "@language": "ga" + }, + { + "@value": "बकरी मीठा दही", + "@language": "hi" + }, + { + "@value": "古塔山谷", + "@language": "zh" + }, + { + "@value": "Goat sweet yogurt", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#goat-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#rabbit", + "rdfs:label": [ + { + "@value": "Lapin", + "@language": "fr" + }, + { + "@value": "Rabbit", + "@language": "de" + }, + { + "@value": "Rabbit", + "@language": "es" + }, + { + "@value": "Rabbit", + "@language": "en" + }, + { + "@value": "الأرنب", + "@language": "ar" + }, + { + "@value": "Rabbit", + "@language": "ku" + }, + { + "@value": "Coniglio", + "@language": "it" + }, + { + "@value": "Rabbit", + "@language": "sw" + }, + { + "@value": "Coelho", + "@language": "pt" + }, + { + "@value": "Rabbit", + "@language": "oc" + }, + { + "@value": "Кролик", + "@language": "ru" + }, + { + "@value": "Rabbit", + "@language": "cy" + }, + { + "@value": "ラビット", + "@language": "ja" + }, + { + "@value": "Uirlisí ilchuspóireacha", + "@language": "ga" + }, + { + "@value": "खरगोश", + "@language": "hi" + }, + { + "@value": "强奸", + "@language": "zh" + }, + { + "@value": "Rabbit", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#meat-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#pie-pastry", + "rdfs:label": [ + { + "@value": "Pâte à tarte", + "@language": "fr" + }, + { + "@value": "Pie Pastry", + "@language": "de" + }, + { + "@value": "Pie Pastry", + "@language": "es" + }, + { + "@value": "Pie Pastry", + "@language": "en" + }, + { + "@value": "Pie Pastry", + "@language": "ar" + }, + { + "@value": "Pie Pastry", + "@language": "ku" + }, + { + "@value": "Pasticceria di torta", + "@language": "it" + }, + { + "@value": "Pie Pastry", + "@language": "sw" + }, + { + "@value": "Pasta de torta", + "@language": "pt" + }, + { + "@value": "Pie Pastry", + "@language": "oc" + }, + { + "@value": "Пчеловечная кондитерская", + "@language": "ru" + }, + { + "@value": "Pie Pastry", + "@language": "cy" + }, + { + "@value": "パイペストリー", + "@language": "ja" + }, + { + "@value": "Pie Pastor", + "@language": "ga" + }, + { + "@value": "पाई पेस्ट्री", + "@language": "hi" + }, + { + "@value": "Pe Pastry", + "@language": "zh" + }, + { + "@value": "Pie Pastry", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#savory-groceries", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#jam", + "rdfs:label": [ + { + "@value": "Confiture", + "@language": "fr" + }, + { + "@value": "Jam", + "@language": "de" + }, + { + "@value": "Jam", + "@language": "es" + }, + { + "@value": "Jam", + "@language": "en" + }, + { + "@value": "Jam", + "@language": "ar" + }, + { + "@value": "Jam", + "@language": "ku" + }, + { + "@value": "Jam", + "@language": "it" + }, + { + "@value": "Jam", + "@language": "sw" + }, + { + "@value": "Jam.", + "@language": "pt" + }, + { + "@value": "Jam", + "@language": "oc" + }, + { + "@value": "Джем", + "@language": "ru" + }, + { + "@value": "Jam", + "@language": "cy" + }, + { + "@value": "ジャム", + "@language": "ja" + }, + { + "@value": "taiseachas aeir: fliuch", + "@language": "ga" + }, + { + "@value": "जाम", + "@language": "hi" + }, + { + "@value": "Jam", + "@language": "zh" + }, + { + "@value": "Jam", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#sweet-groceries", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#fourth-range-vegetable", + "rdfs:label": [ + { + "@value": "Légume quatrième gamme", + "@language": "fr" + }, + { + "@value": "Fourth range vegetable", + "@language": "de" + }, + { + "@value": "Fourth range vegetable", + "@language": "es" + }, + { + "@value": "Fourth range vegetable", + "@language": "en" + }, + { + "@value": "النطاق الرابع", + "@language": "ar" + }, + { + "@value": "Fourth range vegetable", + "@language": "ku" + }, + { + "@value": "Ortaggio di quarta gamma", + "@language": "it" + }, + { + "@value": "Fourth range vegetable", + "@language": "sw" + }, + { + "@value": "Vegetal de quarta gama", + "@language": "pt" + }, + { + "@value": "Fourth range vegetable", + "@language": "oc" + }, + { + "@value": "Четвертый ассортимент овощей", + "@language": "ru" + }, + { + "@value": "Fourth range vegetable", + "@language": "cy" + }, + { + "@value": "野菜の4種類", + "@language": "ja" + }, + { + "@value": "Ceathrú raon glasraí", + "@language": "ga" + }, + { + "@value": "चौथा रेंज सब्जी", + "@language": "hi" + }, + { + "@value": "第四种蔬菜", + "@language": "zh" + }, + { + "@value": "Fourth range vegetable", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#mushroom", + "rdfs:label": [ + { + "@value": "Champignon", + "@language": "fr" + }, + { + "@value": "Mushroom", + "@language": "de" + }, + { + "@value": "Mushroom", + "@language": "es" + }, + { + "@value": "Mushroom", + "@language": "en" + }, + { + "@value": "غرفة الطعام", + "@language": "ar" + }, + { + "@value": "Mushroom", + "@language": "ku" + }, + { + "@value": "Mushroom", + "@language": "it" + }, + { + "@value": "Mushroom", + "@language": "sw" + }, + { + "@value": "Cogumelo", + "@language": "pt" + }, + { + "@value": "Mushroom", + "@language": "oc" + }, + { + "@value": "Гриб", + "@language": "ru" + }, + { + "@value": "Mushroom", + "@language": "cy" + }, + { + "@value": "マッシュルーム", + "@language": "ja" + }, + { + "@value": "tréimhse saoil: ilbhliantúil", + "@language": "ga" + }, + { + "@value": "मशरूम", + "@language": "hi" + }, + { + "@value": "Mushroom", + "@language": "zh" + }, + { + "@value": "Mushroom", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#beer", + "rdfs:label": [ + { + "@value": "Bière", + "@language": "fr" + }, + { + "@value": "Beer", + "@language": "de" + }, + { + "@value": "Beer", + "@language": "es" + }, + { + "@value": "Beer", + "@language": "en" + }, + { + "@value": "البيرة", + "@language": "ar" + }, + { + "@value": "Beer", + "@language": "ku" + }, + { + "@value": "Birra", + "@language": "it" + }, + { + "@value": "Beer", + "@language": "sw" + }, + { + "@value": "Cerveja.", + "@language": "pt" + }, + { + "@value": "Beer", + "@language": "oc" + }, + { + "@value": "Пиво", + "@language": "ru" + }, + { + "@value": "Beer", + "@language": "cy" + }, + { + "@value": "ビール", + "@language": "ja" + }, + { + "@value": "Thosaigh", + "@language": "ga" + }, + { + "@value": "बियर", + "@language": "hi" + }, + { + "@value": "贝 尔", + "@language": "zh" + }, + { + "@value": "Beer", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#alcoholic-beverage", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#chilli-pepper", + "rdfs:label": [ + { + "@value": "Piment", + "@language": "fr" + }, + { + "@value": "Chilli pepper", + "@language": "de" + }, + { + "@value": "Chilli pepper", + "@language": "es" + }, + { + "@value": "Chilli pepper", + "@language": "en" + }, + { + "@value": "فلفل شيلي", + "@language": "ar" + }, + { + "@value": "Chilli pepper", + "@language": "ku" + }, + { + "@value": "Peperoncino peperoncino", + "@language": "it" + }, + { + "@value": "Chilli pepper", + "@language": "sw" + }, + { + "@value": "pimenta de Chilli", + "@language": "pt" + }, + { + "@value": "Chilli pepper", + "@language": "oc" + }, + { + "@value": "Чили перец", + "@language": "ru" + }, + { + "@value": "Chilli pepper", + "@language": "cy" + }, + { + "@value": "唐辛子コショウ", + "@language": "ja" + }, + { + "@value": "Piobar Chilli", + "@language": "ga" + }, + { + "@value": "मिर्च", + "@language": "hi" + }, + { + "@value": "Chillipepper", + "@language": "zh" + }, + { + "@value": "Chilli pepper", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#aromatic", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#non-local-vegetable", + "rdfs:label": [ + { + "@value": "Légume non local", + "@language": "fr" + }, + { + "@value": "Non local vegetable", + "@language": "de" + }, + { + "@value": "Non local vegetable", + "@language": "es" + }, + { + "@value": "Non local vegetable", + "@language": "en" + }, + { + "@value": "الخضار المحلية", + "@language": "ar" + }, + { + "@value": "Non local vegetable", + "@language": "ku" + }, + { + "@value": "Ortaggi non locali", + "@language": "it" + }, + { + "@value": "Non local vegetable", + "@language": "sw" + }, + { + "@value": "Vegetação não local", + "@language": "pt" + }, + { + "@value": "Non local vegetable", + "@language": "oc" + }, + { + "@value": "Не местный овощ", + "@language": "ru" + }, + { + "@value": "Non local vegetable", + "@language": "cy" + }, + { + "@value": "非ローカル野菜", + "@language": "ja" + }, + { + "@value": "Glasraí neamh-díreach", + "@language": "ga" + }, + { + "@value": "गैर स्थानीय सब्जी", + "@language": "hi" + }, + { + "@value": "当地非蔬菜", + "@language": "zh" + }, + { + "@value": "Non local vegetable", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#chicory", + "rdfs:label": [ + { + "@value": "Chicorée", + "@language": "fr" + }, + { + "@value": "Chicory", + "@language": "de" + }, + { + "@value": "Chicory", + "@language": "es" + }, + { + "@value": "Chicory", + "@language": "en" + }, + { + "@value": "تشيكوري", + "@language": "ar" + }, + { + "@value": "Chicory", + "@language": "ku" + }, + { + "@value": "Chicory", + "@language": "it" + }, + { + "@value": "Chicory", + "@language": "sw" + }, + { + "@value": "Chicote", + "@language": "pt" + }, + { + "@value": "Chicory", + "@language": "oc" + }, + { + "@value": "Чикори", + "@language": "ru" + }, + { + "@value": "Chicory", + "@language": "cy" + }, + { + "@value": "シックリー", + "@language": "ja" + }, + { + "@value": "taiseachas aeir: fliuch", + "@language": "ga" + }, + { + "@value": "चिकोरी", + "@language": "hi" + }, + { + "@value": "档案", + "@language": "zh" + }, + { + "@value": "Chicory", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#salad", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#other-cheese", + "rdfs:label": [ + { + "@value": "Fromage", + "@language": "fr" + }, + { + "@value": "Other Cheese", + "@language": "de" + }, + { + "@value": "Other Cheese", + "@language": "es" + }, + { + "@value": "Other Cheese", + "@language": "en" + }, + { + "@value": "جبنة أخرى", + "@language": "ar" + }, + { + "@value": "Other Cheese", + "@language": "ku" + }, + { + "@value": "Altri formaggi", + "@language": "it" + }, + { + "@value": "Other Cheese", + "@language": "sw" + }, + { + "@value": "Outros queijos", + "@language": "pt" + }, + { + "@value": "Other Cheese", + "@language": "oc" + }, + { + "@value": "Другой Сыр", + "@language": "ru" + }, + { + "@value": "Other Cheese", + "@language": "cy" + }, + { + "@value": "その他のチーズ", + "@language": "ja" + }, + { + "@value": "Cáis eile", + "@language": "ga" + }, + { + "@value": "अन्य चीज़ें", + "@language": "hi" + }, + { + "@value": "其他车臣", + "@language": "zh" + }, + { + "@value": "Other Cheese", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#other-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#cream-cheese", + "rdfs:label": [ + { + "@value": "Fromage blanc", + "@language": "fr" + }, + { + "@value": "Cream cheese", + "@language": "de" + }, + { + "@value": "Cream cheese", + "@language": "es" + }, + { + "@value": "Cream cheese", + "@language": "en" + }, + { + "@value": "جبنة كريم", + "@language": "ar" + }, + { + "@value": "Cream cheese", + "@language": "ku" + }, + { + "@value": "Crema di formaggio", + "@language": "it" + }, + { + "@value": "Cream cheese", + "@language": "sw" + }, + { + "@value": "Queijo de creme", + "@language": "pt" + }, + { + "@value": "Cream cheese", + "@language": "oc" + }, + { + "@value": "Сливочный сыр", + "@language": "ru" + }, + { + "@value": "Cream cheese", + "@language": "cy" + }, + { + "@value": "クリームチーズ", + "@language": "ja" + }, + { + "@value": "cáis uachtar uachtar reoite", + "@language": "ga" + }, + { + "@value": "क्रीम पनीर", + "@language": "hi" + }, + { + "@value": "Cream喂养", + "@language": "zh" + }, + { + "@value": "Cream cheese", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#cow-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#peach", + "rdfs:label": [ + { + "@value": "Pêche", + "@language": "fr" + }, + { + "@value": "Peach", + "@language": "de" + }, + { + "@value": "Peach", + "@language": "es" + }, + { + "@value": "Peach", + "@language": "en" + }, + { + "@value": "الشاطئ", + "@language": "ar" + }, + { + "@value": "Peach", + "@language": "ku" + }, + { + "@value": "Pesca", + "@language": "it" + }, + { + "@value": "Peach", + "@language": "sw" + }, + { + "@value": "Pêssego", + "@language": "pt" + }, + { + "@value": "Peach", + "@language": "oc" + }, + { + "@value": "Учете", + "@language": "ru" + }, + { + "@value": "Peach", + "@language": "cy" + }, + { + "@value": "ピーチ", + "@language": "ja" + }, + { + "@value": "An bhfuil a fhios agat", + "@language": "ga" + }, + { + "@value": "पीच", + "@language": "hi" + }, + { + "@value": "导 言", + "@language": "zh" + }, + { + "@value": "Peach", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#fruit", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#almond", + "rdfs:label": [ + { + "@value": "Amande", + "@language": "fr" + }, + { + "@value": "Almond", + "@language": "de" + }, + { + "@value": "Almond", + "@language": "es" + }, + { + "@value": "Almond", + "@language": "en" + }, + { + "@value": "الموند", + "@language": "ar" + }, + { + "@value": "Almond", + "@language": "ku" + }, + { + "@value": "Mandorla", + "@language": "it" + }, + { + "@value": "Almond", + "@language": "sw" + }, + { + "@value": "Amêndoa", + "@language": "pt" + }, + { + "@value": "Almond", + "@language": "oc" + }, + { + "@value": "Алмаз", + "@language": "ru" + }, + { + "@value": "Almond", + "@language": "cy" + }, + { + "@value": "アーモンド", + "@language": "ja" + }, + { + "@value": "Amharc ar gach eolas", + "@language": "ga" + }, + { + "@value": "बादाम", + "@language": "hi" + }, + { + "@value": "Almond", + "@language": "zh" + }, + { + "@value": "Almond", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#nut", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#milk", + "rdfs:label": [ + { + "@value": "Lait", + "@language": "fr" + }, + { + "@value": "Milk", + "@language": "de" + }, + { + "@value": "Milk", + "@language": "es" + }, + { + "@value": "Milk", + "@language": "en" + }, + { + "@value": "حليب", + "@language": "ar" + }, + { + "@value": "Milk", + "@language": "ku" + }, + { + "@value": "Latte", + "@language": "it" + }, + { + "@value": "Milk", + "@language": "sw" + }, + { + "@value": "Leite", + "@language": "pt" + }, + { + "@value": "Milk", + "@language": "oc" + }, + { + "@value": "Молоко", + "@language": "ru" + }, + { + "@value": "Milk", + "@language": "cy" + }, + { + "@value": "ミルク", + "@language": "ja" + }, + { + "@value": "Bainne Bainne", + "@language": "ga" + }, + { + "@value": "दूध", + "@language": "hi" + }, + { + "@value": "Milk", + "@language": "zh" + }, + { + "@value": "Milk", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#cow-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#romanesco-cabbage", + "rdfs:label": [ + { + "@value": "Chou romanesco", + "@language": "fr" + }, + { + "@value": "Romanesco cabbage", + "@language": "de" + }, + { + "@value": "Romanesco cabbage", + "@language": "es" + }, + { + "@value": "Romanesco cabbage", + "@language": "en" + }, + { + "@value": "سيارة أجرة رومانيسكو", + "@language": "ar" + }, + { + "@value": "Romanesco cabbage", + "@language": "ku" + }, + { + "@value": "Cavolo romano", + "@language": "it" + }, + { + "@value": "Romanesco cabbage", + "@language": "sw" + }, + { + "@value": "Repolho de Romanesco", + "@language": "pt" + }, + { + "@value": "Romanesco cabbage", + "@language": "oc" + }, + { + "@value": "Романеско капуста", + "@language": "ru" + }, + { + "@value": "Romanesco cabbage", + "@language": "cy" + }, + { + "@value": "ロマネスコキャベツ", + "@language": "ja" + }, + { + "@value": "Cabáiste Romanesco", + "@language": "ga" + }, + { + "@value": "रोमनको गोभी", + "@language": "hi" + }, + { + "@value": "罗马人协会", + "@language": "zh" + }, + { + "@value": "Romanesco cabbage", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#cabbage", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#frozen", + "rdfs:label": [ + { + "@value": "Surgelé", + "@language": "fr" + }, + { + "@value": "Frozen", + "@language": "de" + }, + { + "@value": "Frozen", + "@language": "es" + }, + { + "@value": "Frozen", + "@language": "en" + }, + { + "@value": "Frozen", + "@language": "ar" + }, + { + "@value": "Frozen", + "@language": "ku" + }, + { + "@value": "congelato", + "@language": "it" + }, + { + "@value": "Frozen", + "@language": "sw" + }, + { + "@value": "Congelado", + "@language": "pt" + }, + { + "@value": "Frozen", + "@language": "oc" + }, + { + "@value": "Замороженные", + "@language": "ru" + }, + { + "@value": "Frozen", + "@language": "cy" + }, + { + "@value": "冷凍庫", + "@language": "ja" + }, + { + "@value": "Frozen", + "@language": "ga" + }, + { + "@value": "फ्रोजन", + "@language": "hi" + }, + { + "@value": "公民", + "@language": "zh" + }, + { + "@value": "Frozen", + "@language": "ca" + } + ], + "dfc-p:specialize": "undefined", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#fresh-meat", + "rdfs:label": [ + { + "@value": "Viande fraîche", + "@language": "fr" + }, + { + "@value": "Fresh meat", + "@language": "de" + }, + { + "@value": "Fresh meat", + "@language": "es" + }, + { + "@value": "Fresh meat", + "@language": "en" + }, + { + "@value": "لحم طازج", + "@language": "ar" + }, + { + "@value": "Fresh meat", + "@language": "ku" + }, + { + "@value": "Carne fresca", + "@language": "it" + }, + { + "@value": "Fresh meat", + "@language": "sw" + }, + { + "@value": "Carne fresca", + "@language": "pt" + }, + { + "@value": "Fresh meat", + "@language": "oc" + }, + { + "@value": "Свежое мясо", + "@language": "ru" + }, + { + "@value": "Fresh meat", + "@language": "cy" + }, + { + "@value": "新鮮な肉", + "@language": "ja" + }, + { + "@value": "Feoil úr", + "@language": "ga" + }, + { + "@value": "ताजा मांस", + "@language": "hi" + }, + { + "@value": "肉类", + "@language": "zh" + }, + { + "@value": "Fresh meat", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#pork", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#semolina", + "rdfs:label": [ + { + "@value": "Semoule", + "@language": "fr" + }, + { + "@value": "Semolina", + "@language": "de" + }, + { + "@value": "Semolina", + "@language": "es" + }, + { + "@value": "Semolina", + "@language": "en" + }, + { + "@value": "Semolina", + "@language": "ar" + }, + { + "@value": "Semolina", + "@language": "ku" + }, + { + "@value": "Semola", + "@language": "it" + }, + { + "@value": "Semolina", + "@language": "sw" + }, + { + "@value": "Semolina", + "@language": "pt" + }, + { + "@value": "Semolina", + "@language": "oc" + }, + { + "@value": "Семолина", + "@language": "ru" + }, + { + "@value": "Semolina", + "@language": "cy" + }, + { + "@value": "セモリナ", + "@language": "ja" + }, + { + "@value": "taiseachas aeir: fliuch", + "@language": "ga" + }, + { + "@value": "सूजी", + "@language": "hi" + }, + { + "@value": "Semolina", + "@language": "zh" + }, + { + "@value": "Semolina", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#savory-groceries", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#turkey", + "rdfs:label": [ + { + "@value": "Dinde", + "@language": "fr" + }, + { + "@value": "Turkey", + "@language": "de" + }, + { + "@value": "Turkey", + "@language": "es" + }, + { + "@value": "Turkey", + "@language": "en" + }, + { + "@value": "تركيا", + "@language": "ar" + }, + { + "@value": "Turkey", + "@language": "ku" + }, + { + "@value": "Turchia", + "@language": "it" + }, + { + "@value": "Turkey", + "@language": "sw" + }, + { + "@value": "Turquia", + "@language": "pt" + }, + { + "@value": "Turkey", + "@language": "oc" + }, + { + "@value": "Турция", + "@language": "ru" + }, + { + "@value": "Turkey", + "@language": "cy" + }, + { + "@value": "トルコ", + "@language": "ja" + }, + { + "@value": "Tuirc", + "@language": "ga" + }, + { + "@value": "तुर्की", + "@language": "hi" + }, + { + "@value": "土耳其", + "@language": "zh" + }, + { + "@value": "Turkey", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#poultry", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#plant", + "rdfs:label": [ + { + "@value": "Plant", + "@language": "fr" + }, + { + "@value": "Plant", + "@language": "de" + }, + { + "@value": "Plant", + "@language": "es" + }, + { + "@value": "Plant", + "@language": "en" + }, + { + "@value": "النباتات", + "@language": "ar" + }, + { + "@value": "Plant", + "@language": "ku" + }, + { + "@value": "Impianto", + "@language": "it" + }, + { + "@value": "Plant", + "@language": "sw" + }, + { + "@value": "Planta", + "@language": "pt" + }, + { + "@value": "Plant", + "@language": "oc" + }, + { + "@value": "Завод", + "@language": "ru" + }, + { + "@value": "Plant", + "@language": "cy" + }, + { + "@value": "プラント", + "@language": "ja" + }, + { + "@value": "Plandaí faoi dhíon", + "@language": "ga" + }, + { + "@value": "संयंत्र", + "@language": "hi" + }, + { + "@value": "计划", + "@language": "zh" + }, + { + "@value": "Plant", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#inedible", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#kale-cabbage", + "rdfs:label": [ + { + "@value": "Chou kale", + "@language": "fr" + }, + { + "@value": "Kale cabbage", + "@language": "de" + }, + { + "@value": "Kale cabbage", + "@language": "es" + }, + { + "@value": "Kale cabbage", + "@language": "en" + }, + { + "@value": "مقصورة كالي", + "@language": "ar" + }, + { + "@value": "Kale cabbage", + "@language": "ku" + }, + { + "@value": "cavolo", + "@language": "it" + }, + { + "@value": "Kale cabbage", + "@language": "sw" + }, + { + "@value": "Repolho de Kale", + "@language": "pt" + }, + { + "@value": "Kale cabbage", + "@language": "oc" + }, + { + "@value": "Кале капуста", + "@language": "ru" + }, + { + "@value": "Kale cabbage", + "@language": "cy" + }, + { + "@value": "ケールキャベツ", + "@language": "ja" + }, + { + "@value": "Cabáiste Kale", + "@language": "ga" + }, + { + "@value": "गोभी", + "@language": "hi" + }, + { + "@value": "Kale cabbage", + "@language": "zh" + }, + { + "@value": "Kale cabbage", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#cabbage", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#asparagus", + "rdfs:label": [ + { + "@value": "Asperge", + "@language": "fr" + }, + { + "@value": "Asparagus", + "@language": "de" + }, + { + "@value": "Asparagus", + "@language": "es" + }, + { + "@value": "Asparagus", + "@language": "en" + }, + { + "@value": "Asparagus", + "@language": "ar" + }, + { + "@value": "Asparagus", + "@language": "ku" + }, + { + "@value": "Asparagi", + "@language": "it" + }, + { + "@value": "Asparagus", + "@language": "sw" + }, + { + "@value": "Espargos", + "@language": "pt" + }, + { + "@value": "Asparagus", + "@language": "oc" + }, + { + "@value": "Аспаргус", + "@language": "ru" + }, + { + "@value": "Asparagus", + "@language": "cy" + }, + { + "@value": "アスパラガス", + "@language": "ja" + }, + { + "@value": "Asparagus", + "@language": "ga" + }, + { + "@value": "Asparagus", + "@language": "hi" + }, + { + "@value": "评 注", + "@language": "zh" + }, + { + "@value": "Asparagus", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#goat-yogurt-with-fruits", + "rdfs:label": [ + { + "@value": "Yaourt aux fruits", + "@language": "fr" + }, + { + "@value": "Goat yogurt with fruits", + "@language": "de" + }, + { + "@value": "Goat yogurt with fruits", + "@language": "es" + }, + { + "@value": "Goat yogurt with fruits", + "@language": "en" + }, + { + "@value": "زبادي مع الفاكهة", + "@language": "ar" + }, + { + "@value": "Goat yogurt with fruits", + "@language": "ku" + }, + { + "@value": "Yogurt con frutta", + "@language": "it" + }, + { + "@value": "Goat yogurt with fruits", + "@language": "sw" + }, + { + "@value": "iogurte de cabra com frutas", + "@language": "pt" + }, + { + "@value": "Goat yogurt with fruits", + "@language": "oc" + }, + { + "@value": "Гоат йогурт с фруктами", + "@language": "ru" + }, + { + "@value": "Goat yogurt with fruits", + "@language": "cy" + }, + { + "@value": "フルーツとヤギヨーグルト", + "@language": "ja" + }, + { + "@value": "Iógart goat le torthaí", + "@language": "ga" + }, + { + "@value": "फलों के साथ बकरी दही", + "@language": "hi" + }, + { + "@value": "Gat yogurt 果", + "@language": "zh" + }, + { + "@value": "Goat yogurt with fruits", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#goat-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#wine", + "rdfs:label": [ + { + "@value": "Vin", + "@language": "fr" + }, + { + "@value": "Wine", + "@language": "de" + }, + { + "@value": "Wine", + "@language": "es" + }, + { + "@value": "Wine", + "@language": "en" + }, + { + "@value": "النبيذ", + "@language": "ar" + }, + { + "@value": "Wine", + "@language": "ku" + }, + { + "@value": "Vino", + "@language": "it" + }, + { + "@value": "Wine", + "@language": "sw" + }, + { + "@value": "Vinho", + "@language": "pt" + }, + { + "@value": "Wine", + "@language": "oc" + }, + { + "@value": "Вино", + "@language": "ru" + }, + { + "@value": "Wine", + "@language": "cy" + }, + { + "@value": "ワイン", + "@language": "ja" + }, + { + "@value": "Fíon", + "@language": "ga" + }, + { + "@value": "वाइन", + "@language": "hi" + }, + { + "@value": "温 哥", + "@language": "zh" + }, + { + "@value": "Wine", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#alcoholic-beverage", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#sheep-yogurt-on-a-bed-of-fruit", + "rdfs:label": [ + { + "@value": "Yaourt sur lit de fruit", + "@language": "fr" + }, + { + "@value": "Sheep yogurt on a bed of fruit", + "@language": "de" + }, + { + "@value": "Sheep yogurt on a bed of fruit", + "@language": "es" + }, + { + "@value": "Sheep yogurt on a bed of fruit", + "@language": "en" + }, + { + "@value": "زبادي الخندق على سرير الفاكهة", + "@language": "ar" + }, + { + "@value": "Sheep yogurt on a bed of fruit", + "@language": "ku" + }, + { + "@value": "Yogurt di pecore su un letto di frutta", + "@language": "it" + }, + { + "@value": "Sheep yogurt on a bed of fruit", + "@language": "sw" + }, + { + "@value": "iogurte de carne em uma cama de fruto", + "@language": "pt" + }, + { + "@value": "Sheep yogurt on a bed of fruit", + "@language": "oc" + }, + { + "@value": "Овцы йогурт на кровати фруктов", + "@language": "ru" + }, + { + "@value": "Sheep yogurt on a bed of fruit", + "@language": "cy" + }, + { + "@value": "羊のヨーグルトは、果物のベッドに", + "@language": "ja" + }, + { + "@value": "Iógart caorach ar leaba torthaí", + "@language": "ga" + }, + { + "@value": "एक बिस्तर पर भेड़ दही", + "@language": "hi" + }, + { + "@value": "她在果园里的羊", + "@language": "zh" + }, + { + "@value": "Sheep yogurt on a bed of fruit", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#sheep-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#crepe-and-galette", + "rdfs:label": [ + { + "@value": "Crêpe et galette", + "@language": "fr" + }, + { + "@value": "Crepe and galette", + "@language": "de" + }, + { + "@value": "Crepe and galette", + "@language": "es" + }, + { + "@value": "Crepe and galette", + "@language": "en" + }, + { + "@value": "الكرب والمشنقة", + "@language": "ar" + }, + { + "@value": "Crepe and galette", + "@language": "ku" + }, + { + "@value": "Crepe e galette", + "@language": "it" + }, + { + "@value": "Crepe and galette", + "@language": "sw" + }, + { + "@value": "Crepe e galette", + "@language": "pt" + }, + { + "@value": "Crepe and galette", + "@language": "oc" + }, + { + "@value": "Креп и галетка", + "@language": "ru" + }, + { + "@value": "Crepe and galette", + "@language": "cy" + }, + { + "@value": "クレープとガレット", + "@language": "ja" + }, + { + "@value": "Crepe agus galette", + "@language": "ga" + }, + { + "@value": "क्रेप और गैलेट", + "@language": "hi" + }, + { + "@value": "Crep andette", + "@language": "zh" + }, + { + "@value": "Crepe and galette", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#savory-groceries", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#goat-dairy-product", + "rdfs:label": [ + { + "@value": "Produits laitiers de chèvre", + "@language": "fr" + }, + { + "@value": "Goat dairy product", + "@language": "de" + }, + { + "@value": "Goat dairy product", + "@language": "es" + }, + { + "@value": "Goat dairy product", + "@language": "en" + }, + { + "@value": "منتجات الألبان المطيرة", + "@language": "ar" + }, + { + "@value": "Goat dairy product", + "@language": "ku" + }, + { + "@value": "Prodotti lattiero-caseari", + "@language": "it" + }, + { + "@value": "Goat dairy product", + "@language": "sw" + }, + { + "@value": "Produtos lácteos de cabra", + "@language": "pt" + }, + { + "@value": "Goat dairy product", + "@language": "oc" + }, + { + "@value": "Козь молочный продукт", + "@language": "ru" + }, + { + "@value": "Goat dairy product", + "@language": "cy" + }, + { + "@value": "ヤギ乳製品製品", + "@language": "ja" + }, + { + "@value": "Táirge déiríochta goat", + "@language": "ga" + }, + { + "@value": "बकरी डेयरी उत्पाद", + "@language": "hi" + }, + { + "@value": "Gat dair产品", + "@language": "zh" + }, + { + "@value": "Goat dairy product", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#oyster-mushroom", + "rdfs:label": [ + { + "@value": "Pleurote", + "@language": "fr" + }, + { + "@value": "Oyster mushroom", + "@language": "de" + }, + { + "@value": "Oyster mushroom", + "@language": "es" + }, + { + "@value": "Oyster mushroom", + "@language": "en" + }, + { + "@value": "أوستر فطر", + "@language": "ar" + }, + { + "@value": "Oyster mushroom", + "@language": "ku" + }, + { + "@value": "Fungo di ostrica", + "@language": "it" + }, + { + "@value": "Oyster mushroom", + "@language": "sw" + }, + { + "@value": "Cogumelo Oyster", + "@language": "pt" + }, + { + "@value": "Oyster mushroom", + "@language": "oc" + }, + { + "@value": "Ойстер гриб", + "@language": "ru" + }, + { + "@value": "Oyster mushroom", + "@language": "cy" + }, + { + "@value": "オイスターマッシュルーム", + "@language": "ja" + }, + { + "@value": "Beacán Oisrí", + "@language": "ga" + }, + { + "@value": "ऑयस्टर मशरूम", + "@language": "hi" + }, + { + "@value": "Oyster Milsh", + "@language": "zh" + }, + { + "@value": "Oyster mushroom", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#mushroom", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#seashell", + "rdfs:label": [ + { + "@value": "Coquillage", + "@language": "fr" + }, + { + "@value": "Seashell", + "@language": "de" + }, + { + "@value": "Seashell", + "@language": "es" + }, + { + "@value": "Seashell", + "@language": "en" + }, + { + "@value": "Seashell", + "@language": "ar" + }, + { + "@value": "Seashell", + "@language": "ku" + }, + { + "@value": "Conchiglia", + "@language": "it" + }, + { + "@value": "Seashell", + "@language": "sw" + }, + { + "@value": "Cintura de segurança", + "@language": "pt" + }, + { + "@value": "Seashell", + "@language": "oc" + }, + { + "@value": "Шелл", + "@language": "ru" + }, + { + "@value": "Seashell", + "@language": "cy" + }, + { + "@value": "シーシェル", + "@language": "ja" + }, + { + "@value": "Suíochán agus", + "@language": "ga" + }, + { + "@value": "Seashell", + "@language": "hi" + }, + { + "@value": "海法", + "@language": "zh" + }, + { + "@value": "Seashell", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#fishery-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#radish", + "rdfs:label": [ + { + "@value": "Radis", + "@language": "fr" + }, + { + "@value": "Radish", + "@language": "de" + }, + { + "@value": "Radish", + "@language": "es" + }, + { + "@value": "Radish", + "@language": "en" + }, + { + "@value": "Radish", + "@language": "ar" + }, + { + "@value": "Radish", + "@language": "ku" + }, + { + "@value": "Raggio", + "@language": "it" + }, + { + "@value": "Radish", + "@language": "sw" + }, + { + "@value": "Radical", + "@language": "pt" + }, + { + "@value": "Radish", + "@language": "oc" + }, + { + "@value": "Радиш", + "@language": "ru" + }, + { + "@value": "Radish", + "@language": "cy" + }, + { + "@value": "ラディッシュ", + "@language": "ja" + }, + { + "@value": "SEIRBHÍSÍ", + "@language": "ga" + }, + { + "@value": "राधिका", + "@language": "hi" + }, + { + "@value": "Radish", + "@language": "zh" + }, + { + "@value": "Radish", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#tomato", + "rdfs:label": [ + { + "@value": "Tomate", + "@language": "fr" + }, + { + "@value": "Tomato", + "@language": "de" + }, + { + "@value": "Tomato", + "@language": "es" + }, + { + "@value": "Tomato", + "@language": "en" + }, + { + "@value": "الطماطم", + "@language": "ar" + }, + { + "@value": "Tomato", + "@language": "ku" + }, + { + "@value": "Pomodoro", + "@language": "it" + }, + { + "@value": "Tomato", + "@language": "sw" + }, + { + "@value": "Tomato", + "@language": "pt" + }, + { + "@value": "Tomato", + "@language": "oc" + }, + { + "@value": "Томат", + "@language": "ru" + }, + { + "@value": "Tomato", + "@language": "cy" + }, + { + "@value": "トマト", + "@language": "ja" + }, + { + "@value": "Trátaí nua", + "@language": "ga" + }, + { + "@value": "टमाटर", + "@language": "hi" + }, + { + "@value": "Tomato", + "@language": "zh" + }, + { + "@value": "Tomato", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#pork", + "rdfs:label": [ + { + "@value": "Porc", + "@language": "fr" + }, + { + "@value": "Pork", + "@language": "de" + }, + { + "@value": "Pork", + "@language": "es" + }, + { + "@value": "Pork", + "@language": "en" + }, + { + "@value": "Pork", + "@language": "ar" + }, + { + "@value": "Pork", + "@language": "ku" + }, + { + "@value": "Carne di maiale", + "@language": "it" + }, + { + "@value": "Pork", + "@language": "sw" + }, + { + "@value": "Porco", + "@language": "pt" + }, + { + "@value": "Pork", + "@language": "oc" + }, + { + "@value": "Свинина", + "@language": "ru" + }, + { + "@value": "Pork", + "@language": "cy" + }, + { + "@value": "ポーク", + "@language": "ja" + }, + { + "@value": "irl - Library Service", + "@language": "ga" + }, + { + "@value": "पोर्क", + "@language": "hi" + }, + { + "@value": "波 克", + "@language": "zh" + }, + { + "@value": "Pork", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#meat-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#thyme", + "rdfs:label": [ + { + "@value": "Thym", + "@language": "fr" + }, + { + "@value": "Thyme", + "@language": "de" + }, + { + "@value": "Thyme", + "@language": "es" + }, + { + "@value": "Thyme", + "@language": "en" + }, + { + "@value": "الشيء", + "@language": "ar" + }, + { + "@value": "Thyme", + "@language": "ku" + }, + { + "@value": "Tu", + "@language": "it" + }, + { + "@value": "Thyme", + "@language": "sw" + }, + { + "@value": "Teor", + "@language": "pt" + }, + { + "@value": "Thyme", + "@language": "oc" + }, + { + "@value": "Тим", + "@language": "ru" + }, + { + "@value": "Thyme", + "@language": "cy" + }, + { + "@value": "ティム", + "@language": "ja" + }, + { + "@value": "Téama", + "@language": "ga" + }, + { + "@value": "थाइम", + "@language": "hi" + }, + { + "@value": "序言", + "@language": "zh" + }, + { + "@value": "Thyme", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#aromatic", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#cider", + "rdfs:label": [ + { + "@value": "Cidre", + "@language": "fr" + }, + { + "@value": "Cider", + "@language": "de" + }, + { + "@value": "Cider", + "@language": "es" + }, + { + "@value": "Cider", + "@language": "en" + }, + { + "@value": "Cider", + "@language": "ar" + }, + { + "@value": "Cider", + "@language": "ku" + }, + { + "@value": "Siderurgia", + "@language": "it" + }, + { + "@value": "Cider", + "@language": "sw" + }, + { + "@value": "Cidra", + "@language": "pt" + }, + { + "@value": "Cider", + "@language": "oc" + }, + { + "@value": "Паук", + "@language": "ru" + }, + { + "@value": "Cider", + "@language": "cy" + }, + { + "@value": "サイダー", + "@language": "ja" + }, + { + "@value": "Uirlisí ilchuspóireacha", + "@language": "ga" + }, + { + "@value": "साइडर", + "@language": "hi" + }, + { + "@value": "目 录", + "@language": "zh" + }, + { + "@value": "Cider", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#alcoholic-beverage", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#mint", + "rdfs:label": [ + { + "@value": "Menthe", + "@language": "fr" + }, + { + "@value": "Mint", + "@language": "de" + }, + { + "@value": "Mint", + "@language": "es" + }, + { + "@value": "Mint", + "@language": "en" + }, + { + "@value": "Mint", + "@language": "ar" + }, + { + "@value": "Mint", + "@language": "ku" + }, + { + "@value": "Min", + "@language": "it" + }, + { + "@value": "Mint", + "@language": "sw" + }, + { + "@value": "Mint", + "@language": "pt" + }, + { + "@value": "Mint", + "@language": "oc" + }, + { + "@value": "Минт", + "@language": "ru" + }, + { + "@value": "Mint", + "@language": "cy" + }, + { + "@value": "ミント", + "@language": "ja" + }, + { + "@value": "foirm duille: líneach", + "@language": "ga" + }, + { + "@value": "मिंट", + "@language": "hi" + }, + { + "@value": "Mint", + "@language": "zh" + }, + { + "@value": "Mint", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#aromatic", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#parsley", + "rdfs:label": [ + { + "@value": "Persil", + "@language": "fr" + }, + { + "@value": "Parsley", + "@language": "de" + }, + { + "@value": "Parsley", + "@language": "es" + }, + { + "@value": "Parsley", + "@language": "en" + }, + { + "@value": "Parsley", + "@language": "ar" + }, + { + "@value": "Parsley", + "@language": "ku" + }, + { + "@value": "Parlo", + "@language": "it" + }, + { + "@value": "Parsley", + "@language": "sw" + }, + { + "@value": "Parsley", + "@language": "pt" + }, + { + "@value": "Parsley", + "@language": "oc" + }, + { + "@value": "Парсли", + "@language": "ru" + }, + { + "@value": "Parsley", + "@language": "cy" + }, + { + "@value": "パセリ", + "@language": "ja" + }, + { + "@value": "Inis dúinn, le do thoil...", + "@language": "ga" + }, + { + "@value": "अजमोद", + "@language": "hi" + }, + { + "@value": "帕斯利", + "@language": "zh" + }, + { + "@value": "Parsley", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#aromatic", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#sheep-fresh-cheese", + "rdfs:label": [ + { + "@value": "Fromage frais", + "@language": "fr" + }, + { + "@value": "Sheep Fresh cheese", + "@language": "de" + }, + { + "@value": "Sheep Fresh cheese", + "@language": "es" + }, + { + "@value": "Sheep Fresh cheese", + "@language": "en" + }, + { + "@value": "جبنة طازجة", + "@language": "ar" + }, + { + "@value": "Sheep Fresh cheese", + "@language": "ku" + }, + { + "@value": "Pecora di formaggio fresco", + "@language": "it" + }, + { + "@value": "Sheep Fresh cheese", + "@language": "sw" + }, + { + "@value": "Queijo fresco de carne", + "@language": "pt" + }, + { + "@value": "Sheep Fresh cheese", + "@language": "oc" + }, + { + "@value": "Овцы Свежий сыр", + "@language": "ru" + }, + { + "@value": "Sheep Fresh cheese", + "@language": "cy" + }, + { + "@value": "羊の新鮮なチーズ", + "@language": "ja" + }, + { + "@value": "Cáis úra Caorach", + "@language": "ga" + }, + { + "@value": "भेड़ ताजा पनीर", + "@language": "hi" + }, + { + "@value": "她", + "@language": "zh" + }, + { + "@value": "Sheep Fresh cheese", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#sheep-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#cow-dairy-product", + "rdfs:label": [ + { + "@value": "Produit laitier de vache", + "@language": "fr" + }, + { + "@value": "Cow dairy product", + "@language": "de" + }, + { + "@value": "Cow dairy product", + "@language": "es" + }, + { + "@value": "Cow dairy product", + "@language": "en" + }, + { + "@value": "منتجات الألبان", + "@language": "ar" + }, + { + "@value": "Cow dairy product", + "@language": "ku" + }, + { + "@value": "Prodotti lattiero-caseari", + "@language": "it" + }, + { + "@value": "Cow dairy product", + "@language": "sw" + }, + { + "@value": "Produtos lácteos de vaca", + "@language": "pt" + }, + { + "@value": "Cow dairy product", + "@language": "oc" + }, + { + "@value": "Корова молочная продукция", + "@language": "ru" + }, + { + "@value": "Cow dairy product", + "@language": "cy" + }, + { + "@value": "牛酪農場プロダクト", + "@language": "ja" + }, + { + "@value": "Táirge déiríochta bó", + "@language": "ga" + }, + { + "@value": "गाय डेयरी उत्पाद", + "@language": "hi" + }, + { + "@value": "Cow dairir产品", + "@language": "zh" + }, + { + "@value": "Cow dairy product", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#jerusalem-artichoke", + "rdfs:label": [ + { + "@value": "Topinambour", + "@language": "fr" + }, + { + "@value": "Jerusalem artichoke", + "@language": "de" + }, + { + "@value": "Jerusalem artichoke", + "@language": "es" + }, + { + "@value": "Jerusalem artichoke", + "@language": "en" + }, + { + "@value": "أثاث القدس", + "@language": "ar" + }, + { + "@value": "Jerusalem artichoke", + "@language": "ku" + }, + { + "@value": "Carciofo di Gerusalemme", + "@language": "it" + }, + { + "@value": "Jerusalem artichoke", + "@language": "sw" + }, + { + "@value": "Alcachofra de Jerusalém", + "@language": "pt" + }, + { + "@value": "Jerusalem artichoke", + "@language": "oc" + }, + { + "@value": "Иерусалимский артишок", + "@language": "ru" + }, + { + "@value": "Jerusalem artichoke", + "@language": "cy" + }, + { + "@value": "エルサレムアーティチョーク", + "@language": "ja" + }, + { + "@value": "Ealaín Iarúsailéim", + "@language": "ga" + }, + { + "@value": "यरूशलेम", + "@language": "hi" + }, + { + "@value": "耶路撒冷艺术品", + "@language": "zh" + }, + { + "@value": "Jerusalem artichoke", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#ripe", + "rdfs:label": [ + { + "@value": "Mûre", + "@language": "fr" + }, + { + "@value": "Ripe", + "@language": "de" + }, + { + "@value": "Ripe", + "@language": "es" + }, + { + "@value": "Ripe", + "@language": "en" + }, + { + "@value": "الشريط", + "@language": "ar" + }, + { + "@value": "Ripe", + "@language": "ku" + }, + { + "@value": "Ripeto", + "@language": "it" + }, + { + "@value": "Ripe", + "@language": "sw" + }, + { + "@value": "Ripadas", + "@language": "pt" + }, + { + "@value": "Ripe", + "@language": "oc" + }, + { + "@value": "Рип", + "@language": "ru" + }, + { + "@value": "Ripe", + "@language": "cy" + }, + { + "@value": "リペ", + "@language": "ja" + }, + { + "@value": "Toir agus Crainn", + "@language": "ga" + }, + { + "@value": "चीर", + "@language": "hi" + }, + { + "@value": "目 录", + "@language": "zh" + }, + { + "@value": "Ripe", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#berry", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#salad-mix", + "rdfs:label": [ + { + "@value": "Mélange salades", + "@language": "fr" + }, + { + "@value": "Salad mix", + "@language": "de" + }, + { + "@value": "Salad mix", + "@language": "es" + }, + { + "@value": "Salad mix", + "@language": "en" + }, + { + "@value": "مزيج سلال", + "@language": "ar" + }, + { + "@value": "Salad mix", + "@language": "ku" + }, + { + "@value": "Miscela di insalata", + "@language": "it" + }, + { + "@value": "Salad mix", + "@language": "sw" + }, + { + "@value": "Mistura de salada", + "@language": "pt" + }, + { + "@value": "Salad mix", + "@language": "oc" + }, + { + "@value": "Салатная смесь", + "@language": "ru" + }, + { + "@value": "Salad mix", + "@language": "cy" + }, + { + "@value": "サラダミックス", + "@language": "ja" + }, + { + "@value": "Meascán salainn", + "@language": "ga" + }, + { + "@value": "सलाद मिश्रण", + "@language": "hi" + }, + { + "@value": "薪金组合", + "@language": "zh" + }, + { + "@value": "Salad mix", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#salad", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#parsnip", + "rdfs:label": [ + { + "@value": "Panais", + "@language": "fr" + }, + { + "@value": "Parsnip", + "@language": "de" + }, + { + "@value": "Parsnip", + "@language": "es" + }, + { + "@value": "Parsnip", + "@language": "en" + }, + { + "@value": "Parsnip", + "@language": "ar" + }, + { + "@value": "Parsnip", + "@language": "ku" + }, + { + "@value": "Parsnip", + "@language": "it" + }, + { + "@value": "Parsnip", + "@language": "sw" + }, + { + "@value": "Parsnip", + "@language": "pt" + }, + { + "@value": "Parsnip", + "@language": "oc" + }, + { + "@value": "Парснип", + "@language": "ru" + }, + { + "@value": "Parsnip", + "@language": "cy" + }, + { + "@value": "パルスニップ", + "@language": "ja" + }, + { + "@value": "tréimhse saoil: ilbhliantúil", + "@language": "ga" + }, + { + "@value": "Parsnip", + "@language": "hi" + }, + { + "@value": "帕斯帕尔", + "@language": "zh" + }, + { + "@value": "Parsnip", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#broccoli-cabbage", + "rdfs:label": [ + { + "@value": "Chou brocoli", + "@language": "fr" + }, + { + "@value": "Broccoli cabbage", + "@language": "de" + }, + { + "@value": "Broccoli cabbage", + "@language": "es" + }, + { + "@value": "Broccoli cabbage", + "@language": "en" + }, + { + "@value": "الكاربة", + "@language": "ar" + }, + { + "@value": "Broccoli cabbage", + "@language": "ku" + }, + { + "@value": "cavolo di broccoli", + "@language": "it" + }, + { + "@value": "Broccoli cabbage", + "@language": "sw" + }, + { + "@value": "Repolho de brócolis", + "@language": "pt" + }, + { + "@value": "Broccoli cabbage", + "@language": "oc" + }, + { + "@value": "Брокколи капуста", + "@language": "ru" + }, + { + "@value": "Broccoli cabbage", + "@language": "cy" + }, + { + "@value": "ブロッコリーキャベツ", + "@language": "ja" + }, + { + "@value": "Cabáiste Brocailí", + "@language": "ga" + }, + { + "@value": "Broccoli गोभी", + "@language": "hi" + }, + { + "@value": "布鲁里纳·卡宾", + "@language": "zh" + }, + { + "@value": "Broccoli cabbage", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#cabbage", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#salt", + "rdfs:label": [ + { + "@value": "Sel", + "@language": "fr" + }, + { + "@value": "Salt", + "@language": "de" + }, + { + "@value": "Salt", + "@language": "es" + }, + { + "@value": "Salt", + "@language": "en" + }, + { + "@value": "الملح", + "@language": "ar" + }, + { + "@value": "Salt", + "@language": "ku" + }, + { + "@value": "Sale", + "@language": "it" + }, + { + "@value": "Salt", + "@language": "sw" + }, + { + "@value": "Sal", + "@language": "pt" + }, + { + "@value": "Salt", + "@language": "oc" + }, + { + "@value": "Соль", + "@language": "ru" + }, + { + "@value": "Salt", + "@language": "cy" + }, + { + "@value": "ソルト", + "@language": "ja" + }, + { + "@value": "Salann", + "@language": "ga" + }, + { + "@value": "नमक", + "@language": "hi" + }, + { + "@value": "薪金", + "@language": "zh" + }, + { + "@value": "Salt", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#savory-groceries", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#yam", + "rdfs:label": [ + { + "@value": "Patate douce", + "@language": "fr" + }, + { + "@value": "Yam", + "@language": "de" + }, + { + "@value": "Yam", + "@language": "es" + }, + { + "@value": "Yam", + "@language": "en" + }, + { + "@value": "يام", + "@language": "ar" + }, + { + "@value": "Yam", + "@language": "ku" + }, + { + "@value": "Yam", + "@language": "it" + }, + { + "@value": "Yam", + "@language": "sw" + }, + { + "@value": "Yam", + "@language": "pt" + }, + { + "@value": "Yam", + "@language": "oc" + }, + { + "@value": "Ям", + "@language": "ru" + }, + { + "@value": "Yam", + "@language": "cy" + }, + { + "@value": "ヤンム", + "@language": "ja" + }, + { + "@value": "Uisce agus Séarachas", + "@language": "ga" + }, + { + "@value": "यमन", + "@language": "hi" + }, + { + "@value": "雅各布", + "@language": "zh" + }, + { + "@value": "Yam", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#sheep-yogurt-with-fruits", + "rdfs:label": [ + { + "@value": "Yaourt aux fruits", + "@language": "fr" + }, + { + "@value": "Sheep yogurt with fruits", + "@language": "de" + }, + { + "@value": "Sheep yogurt with fruits", + "@language": "es" + }, + { + "@value": "Sheep yogurt with fruits", + "@language": "en" + }, + { + "@value": "الزبادي الخراف مع الفاكهة", + "@language": "ar" + }, + { + "@value": "Sheep yogurt with fruits", + "@language": "ku" + }, + { + "@value": "Yogurt di pecore con frutta", + "@language": "it" + }, + { + "@value": "Sheep yogurt with fruits", + "@language": "sw" + }, + { + "@value": "iogurte de carne com frutas", + "@language": "pt" + }, + { + "@value": "Sheep yogurt with fruits", + "@language": "oc" + }, + { + "@value": "Овцы йогурт с фруктами", + "@language": "ru" + }, + { + "@value": "Sheep yogurt with fruits", + "@language": "cy" + }, + { + "@value": "羊のヨーグルトとフルーツ", + "@language": "ja" + }, + { + "@value": "Iógart caorach le torthaí", + "@language": "ga" + }, + { + "@value": "फलों के साथ भेड़ दही", + "@language": "hi" + }, + { + "@value": "她获得果敢的羊", + "@language": "zh" + }, + { + "@value": "Sheep yogurt with fruits", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#sheep-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#green-garlic", + "rdfs:label": [ + { + "@value": "Aillet", + "@language": "fr" + }, + { + "@value": "Green Garlic", + "@language": "de" + }, + { + "@value": "Green Garlic", + "@language": "es" + }, + { + "@value": "Green Garlic", + "@language": "en" + }, + { + "@value": "الثكنة الخضراء", + "@language": "ar" + }, + { + "@value": "Green Garlic", + "@language": "ku" + }, + { + "@value": "Aglio verde", + "@language": "it" + }, + { + "@value": "Green Garlic", + "@language": "sw" + }, + { + "@value": "Alho verde", + "@language": "pt" + }, + { + "@value": "Green Garlic", + "@language": "oc" + }, + { + "@value": "Зеленый чеснок", + "@language": "ru" + }, + { + "@value": "Green Garlic", + "@language": "cy" + }, + { + "@value": "グリーンガーリック", + "@language": "ja" + }, + { + "@value": "Gairleoige glas", + "@language": "ga" + }, + { + "@value": "ग्रीन लहसुन", + "@language": "hi" + }, + { + "@value": "格林加尔", + "@language": "zh" + }, + { + "@value": "Green Garlic", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#chestnut", + "rdfs:label": [ + { + "@value": "Marron", + "@language": "fr" + }, + { + "@value": "Chestnut", + "@language": "de" + }, + { + "@value": "Chestnut", + "@language": "es" + }, + { + "@value": "Chestnut", + "@language": "en" + }, + { + "@value": "Chestnut", + "@language": "ar" + }, + { + "@value": "Chestnut", + "@language": "ku" + }, + { + "@value": "Castagno", + "@language": "it" + }, + { + "@value": "Chestnut", + "@language": "sw" + }, + { + "@value": "Castanho de castanha", + "@language": "pt" + }, + { + "@value": "Chestnut", + "@language": "oc" + }, + { + "@value": "Сундук", + "@language": "ru" + }, + { + "@value": "Chestnut", + "@language": "cy" + }, + { + "@value": "チェストナット", + "@language": "ja" + }, + { + "@value": "riachtanais uisce: measartha", + "@language": "ga" + }, + { + "@value": "चेस्टनट", + "@language": "hi" + }, + { + "@value": "营养不良", + "@language": "zh" + }, + { + "@value": "Chestnut", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#nut", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#chard", + "rdfs:label": [ + { + "@value": "Blette", + "@language": "fr" + }, + { + "@value": "Chard", + "@language": "de" + }, + { + "@value": "Chard", + "@language": "es" + }, + { + "@value": "Chard", + "@language": "en" + }, + { + "@value": "Chard", + "@language": "ar" + }, + { + "@value": "Chard", + "@language": "ku" + }, + { + "@value": "Carica", + "@language": "it" + }, + { + "@value": "Chard", + "@language": "sw" + }, + { + "@value": "Chard", + "@language": "pt" + }, + { + "@value": "Chard", + "@language": "oc" + }, + { + "@value": "Обязанность", + "@language": "ru" + }, + { + "@value": "Chard", + "@language": "cy" + }, + { + "@value": "チャド", + "@language": "ja" + }, + { + "@value": "An t-eolas úsáideach", + "@language": "ga" + }, + { + "@value": "चार्ड", + "@language": "hi" + }, + { + "@value": "慈善社", + "@language": "zh" + }, + { + "@value": "Chard", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#flower", + "rdfs:label": [ + { + "@value": "Fleur", + "@language": "fr" + }, + { + "@value": "Flower", + "@language": "de" + }, + { + "@value": "Flower", + "@language": "es" + }, + { + "@value": "Flower", + "@language": "en" + }, + { + "@value": "الزهور", + "@language": "ar" + }, + { + "@value": "Flower", + "@language": "ku" + }, + { + "@value": "Fiore", + "@language": "it" + }, + { + "@value": "Flower", + "@language": "sw" + }, + { + "@value": "Flores", + "@language": "pt" + }, + { + "@value": "Flower", + "@language": "oc" + }, + { + "@value": "Цветок", + "@language": "ru" + }, + { + "@value": "Flower", + "@language": "cy" + }, + { + "@value": "フラワー", + "@language": "ja" + }, + { + "@value": "cineál gas: in airde", + "@language": "ga" + }, + { + "@value": "फूल", + "@language": "hi" + }, + { + "@value": "研究员", + "@language": "zh" + }, + { + "@value": "Flower", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#inedible", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#fennel", + "rdfs:label": [ + { + "@value": "Fenouil", + "@language": "fr" + }, + { + "@value": "Fennel", + "@language": "de" + }, + { + "@value": "Fennel", + "@language": "es" + }, + { + "@value": "Fennel", + "@language": "en" + }, + { + "@value": "Fennel", + "@language": "ar" + }, + { + "@value": "Fennel", + "@language": "ku" + }, + { + "@value": "Finocchio", + "@language": "it" + }, + { + "@value": "Fennel", + "@language": "sw" + }, + { + "@value": "Fennel", + "@language": "pt" + }, + { + "@value": "Fennel", + "@language": "oc" + }, + { + "@value": "Феннель", + "@language": "ru" + }, + { + "@value": "Fennel", + "@language": "cy" + }, + { + "@value": "フェンネル", + "@language": "ja" + }, + { + "@value": "Fionraí", + "@language": "ga" + }, + { + "@value": "फेनेल", + "@language": "hi" + }, + { + "@value": "Fennel", + "@language": "zh" + }, + { + "@value": "Fennel", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#fish", + "rdfs:label": [ + { + "@value": "Poisson", + "@language": "fr" + }, + { + "@value": "Fish", + "@language": "de" + }, + { + "@value": "Fish", + "@language": "es" + }, + { + "@value": "Fish", + "@language": "en" + }, + { + "@value": "الأسماك", + "@language": "ar" + }, + { + "@value": "Fish", + "@language": "ku" + }, + { + "@value": "Pesce di pesce", + "@language": "it" + }, + { + "@value": "Fish", + "@language": "sw" + }, + { + "@value": "Peixe", + "@language": "pt" + }, + { + "@value": "Fish", + "@language": "oc" + }, + { + "@value": "Рыба", + "@language": "ru" + }, + { + "@value": "Fish", + "@language": "cy" + }, + { + "@value": "魚釣り", + "@language": "ja" + }, + { + "@value": "Éisc Éisc", + "@language": "ga" + }, + { + "@value": "मछली", + "@language": "hi" + }, + { + "@value": "渔业", + "@language": "zh" + }, + { + "@value": "Fish", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#fishery-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#shallot", + "rdfs:label": [ + { + "@value": "Échalote", + "@language": "fr" + }, + { + "@value": "Shallot", + "@language": "de" + }, + { + "@value": "Shallot", + "@language": "es" + }, + { + "@value": "Shallot", + "@language": "en" + }, + { + "@value": "العجلات", + "@language": "ar" + }, + { + "@value": "Shallot", + "@language": "ku" + }, + { + "@value": "Salvo!", + "@language": "it" + }, + { + "@value": "Shallot", + "@language": "sw" + }, + { + "@value": "Votos", + "@language": "pt" + }, + { + "@value": "Shallot", + "@language": "oc" + }, + { + "@value": "Шеллот", + "@language": "ru" + }, + { + "@value": "Shallot", + "@language": "cy" + }, + { + "@value": "シャロット", + "@language": "ja" + }, + { + "@value": "An bhfuil a fhios agat", + "@language": "ga" + }, + { + "@value": "शाललॉट", + "@language": "hi" + }, + { + "@value": "哈洛斯", + "@language": "zh" + }, + { + "@value": "Shallot", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#dried-fruit", + "rdfs:label": [ + { + "@value": "Fruit séché", + "@language": "fr" + }, + { + "@value": "Dried fruit", + "@language": "de" + }, + { + "@value": "Dried fruit", + "@language": "es" + }, + { + "@value": "Dried fruit", + "@language": "en" + }, + { + "@value": "الفاكهة الجافة", + "@language": "ar" + }, + { + "@value": "Dried fruit", + "@language": "ku" + }, + { + "@value": "Frutta secca", + "@language": "it" + }, + { + "@value": "Dried fruit", + "@language": "sw" + }, + { + "@value": "Fruto seco", + "@language": "pt" + }, + { + "@value": "Dried fruit", + "@language": "oc" + }, + { + "@value": "Сушеные фрукты", + "@language": "ru" + }, + { + "@value": "Dried fruit", + "@language": "cy" + }, + { + "@value": "ドライフルーツ", + "@language": "ja" + }, + { + "@value": "Torthaí triomaithe", + "@language": "ga" + }, + { + "@value": "सूखे फल", + "@language": "hi" + }, + { + "@value": "草 果", + "@language": "zh" + }, + { + "@value": "Dried fruit", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#processed-fruit", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#kiwi", + "rdfs:label": [ + { + "@value": "Kiwi", + "@language": "fr" + }, + { + "@value": "Kiwi", + "@language": "de" + }, + { + "@value": "Kiwi", + "@language": "es" + }, + { + "@value": "Kiwi", + "@language": "en" + }, + { + "@value": "كيوي", + "@language": "ar" + }, + { + "@value": "Kiwi", + "@language": "ku" + }, + { + "@value": "Kiwi", + "@language": "it" + }, + { + "@value": "Kiwi", + "@language": "sw" + }, + { + "@value": "Kiwi", + "@language": "pt" + }, + { + "@value": "Kiwi", + "@language": "oc" + }, + { + "@value": "Киви", + "@language": "ru" + }, + { + "@value": "Kiwi", + "@language": "cy" + }, + { + "@value": "キウイ", + "@language": "ja" + }, + { + "@value": "riachtanais uisce: measartha", + "@language": "ga" + }, + { + "@value": "किवी", + "@language": "hi" + }, + { + "@value": "Kwi", + "@language": "zh" + }, + { + "@value": "Kiwi", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#fruit", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#berry", + "rdfs:label": [ + { + "@value": "Petit fruit", + "@language": "fr" + }, + { + "@value": "Berry", + "@language": "de" + }, + { + "@value": "Berry", + "@language": "es" + }, + { + "@value": "Berry", + "@language": "en" + }, + { + "@value": "بيري", + "@language": "ar" + }, + { + "@value": "Berry", + "@language": "ku" + }, + { + "@value": "Berry", + "@language": "it" + }, + { + "@value": "Berry", + "@language": "sw" + }, + { + "@value": "Berry", + "@language": "pt" + }, + { + "@value": "Berry", + "@language": "oc" + }, + { + "@value": "Берри", + "@language": "ru" + }, + { + "@value": "Berry", + "@language": "cy" + }, + { + "@value": "ベリー", + "@language": "ja" + }, + { + "@value": "irl - Library Service", + "@language": "ga" + }, + { + "@value": "बेरी", + "@language": "hi" + }, + { + "@value": "Berry Berry", + "@language": "zh" + }, + { + "@value": "Berry", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#fruit", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#inedible", + "rdfs:label": [ + { + "@value": "Non alimentaire", + "@language": "fr" + }, + { + "@value": "Inedible", + "@language": "de" + }, + { + "@value": "Inedible", + "@language": "es" + }, + { + "@value": "Inedible", + "@language": "en" + }, + { + "@value": "غير قابلة للأكل", + "@language": "ar" + }, + { + "@value": "Inedible", + "@language": "ku" + }, + { + "@value": "Inedito", + "@language": "it" + }, + { + "@value": "Inedible", + "@language": "sw" + }, + { + "@value": "Insuportável", + "@language": "pt" + }, + { + "@value": "Inedible", + "@language": "oc" + }, + { + "@value": "Недопустимо", + "@language": "ru" + }, + { + "@value": "Inedible", + "@language": "cy" + }, + { + "@value": "可愛らしい", + "@language": "ja" + }, + { + "@value": "Doláimhsithe", + "@language": "ga" + }, + { + "@value": "सहज", + "@language": "hi" + }, + { + "@value": "不可接受", + "@language": "zh" + }, + { + "@value": "Inedible", + "@language": "ca" + } + ], + "dfc-p:specialize": "undefined", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#chewed-up", + "rdfs:label": [ + { + "@value": "Mâche", + "@language": "fr" + }, + { + "@value": "Chewed up", + "@language": "de" + }, + { + "@value": "Chewed up", + "@language": "es" + }, + { + "@value": "Chewed up", + "@language": "en" + }, + { + "@value": "تم سحبها", + "@language": "ar" + }, + { + "@value": "Chewed up", + "@language": "ku" + }, + { + "@value": "Masticato", + "@language": "it" + }, + { + "@value": "Chewed up", + "@language": "sw" + }, + { + "@value": "Checos", + "@language": "pt" + }, + { + "@value": "Chewed up", + "@language": "oc" + }, + { + "@value": "Похитили вверх", + "@language": "ru" + }, + { + "@value": "Chewed up", + "@language": "cy" + }, + { + "@value": "巻き上げ", + "@language": "ja" + }, + { + "@value": "Chewed suas", + "@language": "ga" + }, + { + "@value": "चेव्ड अप", + "@language": "hi" + }, + { + "@value": "问题", + "@language": "zh" + }, + { + "@value": "Chewed up", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#salad", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#endive", + "rdfs:label": [ + { + "@value": "Endive", + "@language": "fr" + }, + { + "@value": "Endive", + "@language": "de" + }, + { + "@value": "Endive", + "@language": "es" + }, + { + "@value": "Endive", + "@language": "en" + }, + { + "@value": "Endive", + "@language": "ar" + }, + { + "@value": "Endive", + "@language": "ku" + }, + { + "@value": "Fine", + "@language": "it" + }, + { + "@value": "Endive", + "@language": "sw" + }, + { + "@value": "Fim", + "@language": "pt" + }, + { + "@value": "Endive", + "@language": "oc" + }, + { + "@value": "Конец", + "@language": "ru" + }, + { + "@value": "Endive", + "@language": "cy" + }, + { + "@value": "エクステンション", + "@language": "ja" + }, + { + "@value": "Deireadh an chomhrá", + "@language": "ga" + }, + { + "@value": "एंडिव", + "@language": "hi" + }, + { + "@value": "目的", + "@language": "zh" + }, + { + "@value": "Endive", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#salad", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#celeriac", + "rdfs:label": [ + { + "@value": "Céleri-rave", + "@language": "fr" + }, + { + "@value": "Celeriac", + "@language": "de" + }, + { + "@value": "Celeriac", + "@language": "es" + }, + { + "@value": "Celeriac", + "@language": "en" + }, + { + "@value": "Celeriac", + "@language": "ar" + }, + { + "@value": "Celeriac", + "@language": "ku" + }, + { + "@value": "Celeriac", + "@language": "it" + }, + { + "@value": "Celeriac", + "@language": "sw" + }, + { + "@value": "Celeriac", + "@language": "pt" + }, + { + "@value": "Celeriac", + "@language": "oc" + }, + { + "@value": "Селерия", + "@language": "ru" + }, + { + "@value": "Celeriac", + "@language": "cy" + }, + { + "@value": "セルリアック", + "@language": "ja" + }, + { + "@value": "cliceáil grianghraf a mhéadú", + "@language": "ga" + }, + { + "@value": "सेल्ेरिया", + "@language": "hi" + }, + { + "@value": "标准", + "@language": "zh" + }, + { + "@value": "Celeriac", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#kohlrabi", + "rdfs:label": [ + { + "@value": "Chou rave", + "@language": "fr" + }, + { + "@value": "Kohlrabi", + "@language": "de" + }, + { + "@value": "Kohlrabi", + "@language": "es" + }, + { + "@value": "Kohlrabi", + "@language": "en" + }, + { + "@value": "Kohlrabi", + "@language": "ar" + }, + { + "@value": "Kohlrabi", + "@language": "ku" + }, + { + "@value": "Kohlrabi", + "@language": "it" + }, + { + "@value": "Kohlrabi", + "@language": "sw" + }, + { + "@value": "Kohlrabi", + "@language": "pt" + }, + { + "@value": "Kohlrabi", + "@language": "oc" + }, + { + "@value": "Кольраби", + "@language": "ru" + }, + { + "@value": "Kohlrabi", + "@language": "cy" + }, + { + "@value": "コラビ", + "@language": "ja" + }, + { + "@value": "Cónaidhm na Rúise", + "@language": "ga" + }, + { + "@value": "कोहलीरबी", + "@language": "hi" + }, + { + "@value": "Kohlrabi", + "@language": "zh" + }, + { + "@value": "Kohlrabi", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#cabbage", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#dandelion", + "rdfs:label": [ + { + "@value": "Pissenlit", + "@language": "fr" + }, + { + "@value": "Dandelion", + "@language": "de" + }, + { + "@value": "Dandelion", + "@language": "es" + }, + { + "@value": "Dandelion", + "@language": "en" + }, + { + "@value": "الدانديليون", + "@language": "ar" + }, + { + "@value": "Dandelion", + "@language": "ku" + }, + { + "@value": "Dandelion", + "@language": "it" + }, + { + "@value": "Dandelion", + "@language": "sw" + }, + { + "@value": "Dandelion", + "@language": "pt" + }, + { + "@value": "Dandelion", + "@language": "oc" + }, + { + "@value": "Данделион", + "@language": "ru" + }, + { + "@value": "Dandelion", + "@language": "cy" + }, + { + "@value": "ダンデリオン", + "@language": "ja" + }, + { + "@value": "riachtanais uisce: measartha", + "@language": "ga" + }, + { + "@value": "डेंडिलियन", + "@language": "hi" + }, + { + "@value": "丹德尔", + "@language": "zh" + }, + { + "@value": "Dandelion", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#salad", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#guinea-fowl", + "rdfs:label": [ + { + "@value": "Pintade", + "@language": "fr" + }, + { + "@value": "Guinea fowl", + "@language": "de" + }, + { + "@value": "Guinea fowl", + "@language": "es" + }, + { + "@value": "Guinea fowl", + "@language": "en" + }, + { + "@value": "غينيا", + "@language": "ar" + }, + { + "@value": "Guinea fowl", + "@language": "ku" + }, + { + "@value": "Fascicolo della Guinea", + "@language": "it" + }, + { + "@value": "Guinea fowl", + "@language": "sw" + }, + { + "@value": "Patrulha da Guiné", + "@language": "pt" + }, + { + "@value": "Guinea fowl", + "@language": "oc" + }, + { + "@value": "Гвинея фольва", + "@language": "ru" + }, + { + "@value": "Guinea fowl", + "@language": "cy" + }, + { + "@value": "ギニアフクロウ", + "@language": "ja" + }, + { + "@value": "Éanc na Guine", + "@language": "ga" + }, + { + "@value": "गिनी फाउल", + "@language": "hi" + }, + { + "@value": "几内亚", + "@language": "zh" + }, + { + "@value": "Guinea fowl", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#poultry", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#cassis", + "rdfs:label": [ + { + "@value": "Cassis", + "@language": "fr" + }, + { + "@value": "Cassis", + "@language": "de" + }, + { + "@value": "Cassis", + "@language": "es" + }, + { + "@value": "Cassis", + "@language": "en" + }, + { + "@value": "كاسيس", + "@language": "ar" + }, + { + "@value": "Cassis", + "@language": "ku" + }, + { + "@value": "Cassis", + "@language": "it" + }, + { + "@value": "Cassis", + "@language": "sw" + }, + { + "@value": "Cassis", + "@language": "pt" + }, + { + "@value": "Cassis", + "@language": "oc" + }, + { + "@value": "Кассис", + "@language": "ru" + }, + { + "@value": "Cassis", + "@language": "cy" + }, + { + "@value": "カシス", + "@language": "ja" + }, + { + "@value": "irl - Library Service", + "@language": "ga" + }, + { + "@value": "कैसिस", + "@language": "hi" + }, + { + "@value": "Cassis", + "@language": "zh" + }, + { + "@value": "Cassis", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#berry", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#duck", + "rdfs:label": [ + { + "@value": "Canard", + "@language": "fr" + }, + { + "@value": "Duck", + "@language": "de" + }, + { + "@value": "Duck", + "@language": "es" + }, + { + "@value": "Duck", + "@language": "en" + }, + { + "@value": "داك", + "@language": "ar" + }, + { + "@value": "Duck", + "@language": "ku" + }, + { + "@value": "Ducky", + "@language": "it" + }, + { + "@value": "Duck", + "@language": "sw" + }, + { + "@value": "Duck.", + "@language": "pt" + }, + { + "@value": "Duck", + "@language": "oc" + }, + { + "@value": "Утка", + "@language": "ru" + }, + { + "@value": "Duck", + "@language": "cy" + }, + { + "@value": "ダック", + "@language": "ja" + }, + { + "@value": "Duck Duck Duck", + "@language": "ga" + }, + { + "@value": "डक", + "@language": "hi" + }, + { + "@value": "Duck", + "@language": "zh" + }, + { + "@value": "Duck", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#poultry", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#deaths-trumpet", + "rdfs:label": [ + { + "@value": "Trompette de la mort", + "@language": "fr" + }, + { + "@value": "Death's trumpet", + "@language": "de" + }, + { + "@value": "Death's trumpet", + "@language": "es" + }, + { + "@value": "Death's trumpet", + "@language": "en" + }, + { + "@value": "الموت متشرد", + "@language": "ar" + }, + { + "@value": "Death's trumpet", + "@language": "ku" + }, + { + "@value": "La tromba della morte", + "@language": "it" + }, + { + "@value": "Death's trumpet", + "@language": "sw" + }, + { + "@value": "Trompete da morte", + "@language": "pt" + }, + { + "@value": "Death's trumpet", + "@language": "oc" + }, + { + "@value": "Трампец смерти", + "@language": "ru" + }, + { + "@value": "Death's trumpet", + "@language": "cy" + }, + { + "@value": "死のトランペット", + "@language": "ja" + }, + { + "@value": "trumpa an bháis", + "@language": "ga" + }, + { + "@value": "मौत की तुरही", + "@language": "hi" + }, + { + "@value": "A. 死难", + "@language": "zh" + }, + { + "@value": "Death's trumpet", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#mushroom", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#cosmetic", + "rdfs:label": [ + { + "@value": "Cosmétique", + "@language": "fr" + }, + { + "@value": "Cosmetic", + "@language": "en" + }, + { + "@value": "Cosmetic", + "@language": "ar" + }, + { + "@value": "Cosmetic", + "@language": "ku" + }, + { + "@value": "Cosméticos", + "@language": "es" + }, + { + "@value": "Cosmetici", + "@language": "it" + }, + { + "@value": "Kosmetika", + "@language": "de" + }, + { + "@value": "Cosmetic", + "@language": "sw" + }, + { + "@value": "Cosméticos", + "@language": "pt" + }, + { + "@value": "Cosmetic", + "@language": "oc" + }, + { + "@value": "Косметика", + "@language": "ru" + }, + { + "@value": "Cosmetic", + "@language": "cy" + }, + { + "@value": "化粧品", + "@language": "ja" + }, + { + "@value": "Cosmaideacha", + "@language": "ga" + }, + { + "@value": "कॉस्मेटिक", + "@language": "hi" + }, + { + "@value": "遗传学", + "@language": "zh" + }, + { + "@value": "Cosmetic", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#inedible", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#festive-poultry", + "rdfs:label": [ + { + "@value": "Volaille festive", + "@language": "fr" + }, + { + "@value": "Festive poultry", + "@language": "en" + }, + { + "@value": "الدواجن الإيجابية", + "@language": "ar" + }, + { + "@value": "Festive poultry", + "@language": "ku" + }, + { + "@value": "Población festiva", + "@language": "es" + }, + { + "@value": "Pollame festivo", + "@language": "it" + }, + { + "@value": "Festgeflügel", + "@language": "de" + }, + { + "@value": "Festive poultry", + "@language": "sw" + }, + { + "@value": "Aves de capoeira", + "@language": "pt" + }, + { + "@value": "Festive poultry", + "@language": "oc" + }, + { + "@value": "Праздничная птица", + "@language": "ru" + }, + { + "@value": "Festive poultry", + "@language": "cy" + }, + { + "@value": "お祝いの養鶏", + "@language": "ja" + }, + { + "@value": "éanlaith chlóis", + "@language": "ga" + }, + { + "@value": "Festive पोल्ट्री", + "@language": "hi" + }, + { + "@value": "最富有的家禽", + "@language": "zh" + }, + { + "@value": "Festive poultry", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#poultry", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#melon", + "rdfs:label": [ + { + "@value": "Melon", + "@language": "fr" + }, + { + "@value": "Melon", + "@language": "en" + }, + { + "@value": "ميلون", + "@language": "ar" + }, + { + "@value": "Melon", + "@language": "ku" + }, + { + "@value": "Melon", + "@language": "es" + }, + { + "@value": "Melon", + "@language": "it" + }, + { + "@value": "Melonen", + "@language": "de" + }, + { + "@value": "Melon", + "@language": "sw" + }, + { + "@value": "Melon.", + "@language": "pt" + }, + { + "@value": "Melon", + "@language": "oc" + }, + { + "@value": "Мелон", + "@language": "ru" + }, + { + "@value": "Melon", + "@language": "cy" + }, + { + "@value": "メロン", + "@language": "ja" + }, + { + "@value": "Toir agus Crainn", + "@language": "ga" + }, + { + "@value": "मेलोन", + "@language": "hi" + }, + { + "@value": "梅纳", + "@language": "zh" + }, + { + "@value": "Melon", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#sweet-groceries", + "rdfs:label": [ + { + "@value": "Epicerie sucrée", + "@language": "fr" + }, + { + "@value": "Sweet groceries", + "@language": "en" + }, + { + "@value": "البقالة الحلوة", + "@language": "ar" + }, + { + "@value": "Sweet groceries", + "@language": "ku" + }, + { + "@value": "Dulces comestibles", + "@language": "es" + }, + { + "@value": "Ciliegie dolci", + "@language": "it" + }, + { + "@value": "Süße Lebensmittel", + "@language": "de" + }, + { + "@value": "Sweet groceries", + "@language": "sw" + }, + { + "@value": "Doces doces", + "@language": "pt" + }, + { + "@value": "Sweet groceries", + "@language": "oc" + }, + { + "@value": "Сладкие продукты", + "@language": "ru" + }, + { + "@value": "Sweet groceries", + "@language": "cy" + }, + { + "@value": "甘い食料品", + "@language": "ja" + }, + { + "@value": "Glóthach Milis", + "@language": "ga" + }, + { + "@value": "मिठाई किराने की", + "@language": "hi" + }, + { + "@value": "Sweet 海洋学", + "@language": "zh" + }, + { + "@value": "Sweet groceries", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#local-grocery-store", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#smoothie", + "rdfs:label": [ + { + "@value": "Smoothie", + "@language": "fr" + }, + { + "@value": "Smoothie", + "@language": "en" + }, + { + "@value": "Smoothie", + "@language": "ar" + }, + { + "@value": "Smoothie", + "@language": "ku" + }, + { + "@value": "Smoothie", + "@language": "es" + }, + { + "@value": "Smoothie", + "@language": "it" + }, + { + "@value": "Smoothe", + "@language": "de" + }, + { + "@value": "Smoothie", + "@language": "sw" + }, + { + "@value": "Suavidade", + "@language": "pt" + }, + { + "@value": "Smoothie", + "@language": "oc" + }, + { + "@value": "Гладкая", + "@language": "ru" + }, + { + "@value": "Smoothie", + "@language": "cy" + }, + { + "@value": "スムージー", + "@language": "ja" + }, + { + "@value": "bláthanna cumhra: cumhráin", + "@language": "ga" + }, + { + "@value": "स्मूथी", + "@language": "hi" + }, + { + "@value": "Smoothie", + "@language": "zh" + }, + { + "@value": "Smoothie", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#soft-drink", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#blueberry", + "rdfs:label": [ + { + "@value": "Myrtille", + "@language": "fr" + }, + { + "@value": "Blueberry", + "@language": "en" + }, + { + "@value": "بلوبيري", + "@language": "ar" + }, + { + "@value": "Blueberry", + "@language": "ku" + }, + { + "@value": "Blueberry", + "@language": "es" + }, + { + "@value": "Mirtillo", + "@language": "it" + }, + { + "@value": "Blaubeeren", + "@language": "de" + }, + { + "@value": "Blueberry", + "@language": "sw" + }, + { + "@value": "Amora", + "@language": "pt" + }, + { + "@value": "Blueberry", + "@language": "oc" + }, + { + "@value": "Блюква", + "@language": "ru" + }, + { + "@value": "Blueberry", + "@language": "cy" + }, + { + "@value": "ブルーベリー", + "@language": "ja" + }, + { + "@value": "taiseachas aeir: fliuch", + "@language": "ga" + }, + { + "@value": "ब्लूबेरी", + "@language": "hi" + }, + { + "@value": "蓝色", + "@language": "zh" + }, + { + "@value": "Blueberry", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#berry", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#gooseberry", + "rdfs:label": [ + { + "@value": "Groseille à maquereau", + "@language": "fr" + }, + { + "@value": "Gooseberry", + "@language": "en" + }, + { + "@value": "Gooseberry", + "@language": "ar" + }, + { + "@value": "Gooseberry", + "@language": "ku" + }, + { + "@value": "Gooseberry", + "@language": "es" + }, + { + "@value": "Ovo", + "@language": "it" + }, + { + "@value": "Erdbeeren", + "@language": "de" + }, + { + "@value": "Gooseberry", + "@language": "sw" + }, + { + "@value": "Amora", + "@language": "pt" + }, + { + "@value": "Gooseberry", + "@language": "oc" + }, + { + "@value": "Гусьяри", + "@language": "ru" + }, + { + "@value": "Gooseberry", + "@language": "cy" + }, + { + "@value": "ゴースベリー", + "@language": "ja" + }, + { + "@value": "taiseachas aeir: fliuch", + "@language": "ga" + }, + { + "@value": "गोज़बेरी", + "@language": "hi" + }, + { + "@value": "Goseberry", + "@language": "zh" + }, + { + "@value": "Gooseberry", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#berry", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#girolle-mushroom", + "rdfs:label": [ + { + "@value": "Girolle", + "@language": "fr" + }, + { + "@value": "Girolle mushroom", + "@language": "en" + }, + { + "@value": "مطرة غيلور", + "@language": "ar" + }, + { + "@value": "Girolle mushroom", + "@language": "ku" + }, + { + "@value": "Setas Girolle", + "@language": "es" + }, + { + "@value": "Funghi di Girolle", + "@language": "it" + }, + { + "@value": "Pilze", + "@language": "de" + }, + { + "@value": "Girolle mushroom", + "@language": "sw" + }, + { + "@value": "Cogumelo Girolle", + "@language": "pt" + }, + { + "@value": "Girolle mushroom", + "@language": "oc" + }, + { + "@value": "Грибный гриб", + "@language": "ru" + }, + { + "@value": "Girolle mushroom", + "@language": "cy" + }, + { + "@value": "ジローレキノコ", + "@language": "ja" + }, + { + "@value": "Beacán Girollach", + "@language": "ga" + }, + { + "@value": "गिरोरी मशरूम", + "@language": "hi" + }, + { + "@value": "Grolle Milshroom", + "@language": "zh" + }, + { + "@value": "Girolle mushroom", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#mushroom", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#grape", + "rdfs:label": [ + { + "@value": "Raisin", + "@language": "fr" + }, + { + "@value": "Grape", + "@language": "en" + }, + { + "@value": "Grape", + "@language": "ar" + }, + { + "@value": "Grape", + "@language": "ku" + }, + { + "@value": "Grape", + "@language": "es" + }, + { + "@value": "Uva", + "@language": "it" + }, + { + "@value": "Getreide", + "@language": "de" + }, + { + "@value": "Grape", + "@language": "sw" + }, + { + "@value": "Grape", + "@language": "pt" + }, + { + "@value": "Grape", + "@language": "oc" + }, + { + "@value": "Грейп", + "@language": "ru" + }, + { + "@value": "Grape", + "@language": "cy" + }, + { + "@value": "グレープ", + "@language": "ja" + }, + { + "@value": "Grád", + "@language": "ga" + }, + { + "@value": "अंगूर", + "@language": "hi" + }, + { + "@value": "逐步", + "@language": "zh" + }, + { + "@value": "Grape", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#fruit", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#aromatic", + "rdfs:label": [ + { + "@value": "Aromate", + "@language": "fr" + }, + { + "@value": "Aromatic", + "@language": "en" + }, + { + "@value": "Aromatic", + "@language": "ar" + }, + { + "@value": "Aromatic", + "@language": "ku" + }, + { + "@value": "Aromatic", + "@language": "es" + }, + { + "@value": "Aromatiche", + "@language": "it" + }, + { + "@value": "Aromaten", + "@language": "de" + }, + { + "@value": "Aromatic", + "@language": "sw" + }, + { + "@value": "Aromática", + "@language": "pt" + }, + { + "@value": "Aromatic", + "@language": "oc" + }, + { + "@value": "Аромат", + "@language": "ru" + }, + { + "@value": "Aromatic", + "@language": "cy" + }, + { + "@value": "アロマティック", + "@language": "ja" + }, + { + "@value": "Amharc ar gach eolas", + "@language": "ga" + }, + { + "@value": "सुगंधित", + "@language": "hi" + }, + { + "@value": "A. 结构", + "@language": "zh" + }, + { + "@value": "Aromatic", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#goat-fresh-cheese", + "rdfs:label": [ + { + "@value": "Fromage frais", + "@language": "fr" + }, + { + "@value": "Goat Fresh cheese", + "@language": "en" + }, + { + "@value": "جبنة فراش", + "@language": "ar" + }, + { + "@value": "Goat Fresh cheese", + "@language": "ku" + }, + { + "@value": "Queso fresco", + "@language": "es" + }, + { + "@value": "Formaggi freschi", + "@language": "it" + }, + { + "@value": "Ziegenkäse", + "@language": "de" + }, + { + "@value": "Goat Fresh cheese", + "@language": "sw" + }, + { + "@value": "Cabra Queijo fresco", + "@language": "pt" + }, + { + "@value": "Goat Fresh cheese", + "@language": "oc" + }, + { + "@value": "Свежий сыр", + "@language": "ru" + }, + { + "@value": "Goat Fresh cheese", + "@language": "cy" + }, + { + "@value": "ゴートフレッシュチーズ", + "@language": "ja" + }, + { + "@value": "Cáis goat Fres", + "@language": "ga" + }, + { + "@value": "बकरी ताजा पनीर", + "@language": "hi" + }, + { + "@value": "Gat Fresh奶粉", + "@language": "zh" + }, + { + "@value": "Goat Fresh cheese", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#goat-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#sheep-flavored-yogurt", + "rdfs:label": [ + { + "@value": "Yaourt aromatisé", + "@language": "fr" + }, + { + "@value": "Sheep flavored yogurt", + "@language": "en" + }, + { + "@value": "الزبادي النكهة", + "@language": "ar" + }, + { + "@value": "Sheep flavored yogurt", + "@language": "ku" + }, + { + "@value": "Oveja de yogur con sabor", + "@language": "es" + }, + { + "@value": "Pecora yogurt aromatizzato", + "@language": "it" + }, + { + "@value": "Schaf aromatisierter Joghurt", + "@language": "de" + }, + { + "@value": "Sheep flavored yogurt", + "@language": "sw" + }, + { + "@value": "Iogurte de sabor de carneiro", + "@language": "pt" + }, + { + "@value": "Sheep flavored yogurt", + "@language": "oc" + }, + { + "@value": "Овцы ароматизированный йогурт", + "@language": "ru" + }, + { + "@value": "Sheep flavored yogurt", + "@language": "cy" + }, + { + "@value": "羊風味ヨーグルト", + "@language": "ja" + }, + { + "@value": "Caorach iógart blas", + "@language": "ga" + }, + { + "@value": "भेड़ स्वाद दही", + "@language": "hi" + }, + { + "@value": "她强奸妻子", + "@language": "zh" + }, + { + "@value": "Sheep flavored yogurt", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#sheep-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#quinoa", + "rdfs:label": [ + { + "@value": "Quinoa", + "@language": "fr" + }, + { + "@value": "Quinoa", + "@language": "en" + }, + { + "@value": "Quinoa", + "@language": "ar" + }, + { + "@value": "Quinoa", + "@language": "ku" + }, + { + "@value": "Quinoa", + "@language": "es" + }, + { + "@value": "Quinoa", + "@language": "it" + }, + { + "@value": "Quinos", + "@language": "de" + }, + { + "@value": "Quinoa", + "@language": "sw" + }, + { + "@value": "Quinoa", + "@language": "pt" + }, + { + "@value": "Quinoa", + "@language": "oc" + }, + { + "@value": "Квинина", + "@language": "ru" + }, + { + "@value": "Quinoa", + "@language": "cy" + }, + { + "@value": "キノア", + "@language": "ja" + }, + { + "@value": "bláthanna cumhra: cumhráin", + "@language": "ga" + }, + { + "@value": "Quinoa", + "@language": "hi" + }, + { + "@value": "魁北克", + "@language": "zh" + }, + { + "@value": "Quinoa", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#dried-vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#ready-meal", + "rdfs:label": [ + { + "@value": "Plat cuisiné", + "@language": "fr" + }, + { + "@value": "Ready meal", + "@language": "en" + }, + { + "@value": "وجبة جاهزة", + "@language": "ar" + }, + { + "@value": "Ready meal", + "@language": "ku" + }, + { + "@value": "Comida lista", + "@language": "es" + }, + { + "@value": "Pasto pronto", + "@language": "it" + }, + { + "@value": "Fertiggericht", + "@language": "de" + }, + { + "@value": "Ready meal", + "@language": "sw" + }, + { + "@value": "Pronta refeição", + "@language": "pt" + }, + { + "@value": "Ready meal", + "@language": "oc" + }, + { + "@value": "Готовая еда", + "@language": "ru" + }, + { + "@value": "Ready meal", + "@language": "cy" + }, + { + "@value": "お食事", + "@language": "ja" + }, + { + "@value": "Gnéas béil", + "@language": "ga" + }, + { + "@value": "तैयार भोजन", + "@language": "hi" + }, + { + "@value": "阅读餐", + "@language": "zh" + }, + { + "@value": "Ready meal", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#local-grocery-store", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#rhubarb", + "rdfs:label": [ + { + "@value": "Rhubarbe", + "@language": "fr" + }, + { + "@value": "Rhubarb", + "@language": "en" + }, + { + "@value": "روبارب", + "@language": "ar" + }, + { + "@value": "Rhubarb", + "@language": "ku" + }, + { + "@value": "Rhubarb", + "@language": "es" + }, + { + "@value": "Rhubarb", + "@language": "it" + }, + { + "@value": "Rhabar", + "@language": "de" + }, + { + "@value": "Rhubarb", + "@language": "sw" + }, + { + "@value": "Rhubarb", + "@language": "pt" + }, + { + "@value": "Rhubarb", + "@language": "oc" + }, + { + "@value": "Рюбарб", + "@language": "ru" + }, + { + "@value": "Rhubarb", + "@language": "cy" + }, + { + "@value": "ラバーブ", + "@language": "ja" + }, + { + "@value": "Rhubarb", + "@language": "ga" + }, + { + "@value": "रघुबार", + "@language": "hi" + }, + { + "@value": "Rhubarb", + "@language": "zh" + }, + { + "@value": "Rhubarb", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#rosemary", + "rdfs:label": [ + { + "@value": "Romarin", + "@language": "fr" + }, + { + "@value": "Rosemary", + "@language": "en" + }, + { + "@value": "Rosemary", + "@language": "ar" + }, + { + "@value": "Rosemary", + "@language": "ku" + }, + { + "@value": "Rosemary", + "@language": "es" + }, + { + "@value": "Rosemary", + "@language": "it" + }, + { + "@value": "Rosen", + "@language": "de" + }, + { + "@value": "Rosemary", + "@language": "sw" + }, + { + "@value": "Rosemary", + "@language": "pt" + }, + { + "@value": "Rosemary", + "@language": "oc" + }, + { + "@value": "Розмари", + "@language": "ru" + }, + { + "@value": "Rosemary", + "@language": "cy" + }, + { + "@value": "ローズマリー", + "@language": "ja" + }, + { + "@value": "riachtanais uisce: measartha", + "@language": "ga" + }, + { + "@value": "रोज़मेरी", + "@language": "hi" + }, + { + "@value": "罗森", + "@language": "zh" + }, + { + "@value": "Rosemary", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#aromatic", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#chinese-cabbage", + "rdfs:label": [ + { + "@value": "Chou chinois", + "@language": "fr" + }, + { + "@value": "Chinese cabbage", + "@language": "en" + }, + { + "@value": "مقصورة صينية", + "@language": "ar" + }, + { + "@value": "Chinese cabbage", + "@language": "ku" + }, + { + "@value": "Caballo chino", + "@language": "es" + }, + { + "@value": "cavolo cinese", + "@language": "it" + }, + { + "@value": "Chinakohl", + "@language": "de" + }, + { + "@value": "Chinese cabbage", + "@language": "sw" + }, + { + "@value": "Repolho chinês", + "@language": "pt" + }, + { + "@value": "Chinese cabbage", + "@language": "oc" + }, + { + "@value": "Китайская капуста", + "@language": "ru" + }, + { + "@value": "Chinese cabbage", + "@language": "cy" + }, + { + "@value": "中国のキャベツ", + "@language": "ja" + }, + { + "@value": "Cabáiste na Síne", + "@language": "ga" + }, + { + "@value": "चीनी गोभी", + "@language": "hi" + }, + { + "@value": "中国慈善社", + "@language": "zh" + }, + { + "@value": "Chinese cabbage", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#cabbage", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#brussels-sprouts", + "rdfs:label": [ + { + "@value": "Chou de Bruxelles", + "@language": "fr" + }, + { + "@value": "Brussels sprouts", + "@language": "en" + }, + { + "@value": "برواسب بروكسل", + "@language": "ar" + }, + { + "@value": "Brussels sprouts", + "@language": "ku" + }, + { + "@value": "Bruselas brotes", + "@language": "es" + }, + { + "@value": "Bruxelles germogli", + "@language": "it" + }, + { + "@value": "Rosenkohl", + "@language": "de" + }, + { + "@value": "Brussels sprouts", + "@language": "sw" + }, + { + "@value": "Aberturas de Bruxelas", + "@language": "pt" + }, + { + "@value": "Brussels sprouts", + "@language": "oc" + }, + { + "@value": "Брюссельские спрятки", + "@language": "ru" + }, + { + "@value": "Brussels sprouts", + "@language": "cy" + }, + { + "@value": "ブリュッセルのスプート", + "@language": "ja" + }, + { + "@value": "Bruscar na Bruiséile", + "@language": "ga" + }, + { + "@value": "ब्रसेल्स अंकुरित", + "@language": "hi" + }, + { + "@value": "B. 布鲁塞尔投机", + "@language": "zh" + }, + { + "@value": "Brussels sprouts", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#cabbage", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#alcoholic-beverage", + "rdfs:label": [ + { + "@value": "Boisson alcoolisée", + "@language": "fr" + }, + { + "@value": "Alcoholic beverage", + "@language": "en" + }, + { + "@value": "المشروبات الكحولية", + "@language": "ar" + }, + { + "@value": "Alcoholic beverage", + "@language": "ku" + }, + { + "@value": "Bebida alcohólica", + "@language": "es" + }, + { + "@value": "Bevande alcoliche", + "@language": "it" + }, + { + "@value": "Alkoholische Getränke", + "@language": "de" + }, + { + "@value": "Alcoholic beverage", + "@language": "sw" + }, + { + "@value": "Bebidas alcoólicas", + "@language": "pt" + }, + { + "@value": "Alcoholic beverage", + "@language": "oc" + }, + { + "@value": "Алкогольные напитки", + "@language": "ru" + }, + { + "@value": "Alcoholic beverage", + "@language": "cy" + }, + { + "@value": "アルコール飲料", + "@language": "ja" + }, + { + "@value": "Deoch alcólach", + "@language": "ga" + }, + { + "@value": "शराबी पेय", + "@language": "hi" + }, + { + "@value": "酗酒", + "@language": "zh" + }, + { + "@value": "Alcoholic beverage", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#drink", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#paris-mushroom", + "rdfs:label": [ + { + "@value": "Champignon de Paris", + "@language": "fr" + }, + { + "@value": "Paris mushroom", + "@language": "en" + }, + { + "@value": "فطر باريس", + "@language": "ar" + }, + { + "@value": "Paris mushroom", + "@language": "ku" + }, + { + "@value": "Setas de París", + "@language": "es" + }, + { + "@value": "Fungo di Parigi", + "@language": "it" + }, + { + "@value": "Pariser Pilz", + "@language": "de" + }, + { + "@value": "Paris mushroom", + "@language": "sw" + }, + { + "@value": "Cogumelo de Paris", + "@language": "pt" + }, + { + "@value": "Paris mushroom", + "@language": "oc" + }, + { + "@value": "Парижский гриб", + "@language": "ru" + }, + { + "@value": "Paris mushroom", + "@language": "cy" + }, + { + "@value": "パリのマッシュルーム", + "@language": "ja" + }, + { + "@value": "Beacán Paris", + "@language": "ga" + }, + { + "@value": "पेरिस मशरूम", + "@language": "hi" + }, + { + "@value": "巴黎马什图", + "@language": "zh" + }, + { + "@value": "Paris mushroom", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#mushroom", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#bottled-fruit", + "rdfs:label": [ + { + "@value": "Fruit en bocal", + "@language": "fr" + }, + { + "@value": "Bottled fruit", + "@language": "en" + }, + { + "@value": "الفاكهة المزروعة", + "@language": "ar" + }, + { + "@value": "Bottled fruit", + "@language": "ku" + }, + { + "@value": "Fruta embotellada", + "@language": "es" + }, + { + "@value": "Frutta in bottiglia", + "@language": "it" + }, + { + "@value": "Früchte", + "@language": "de" + }, + { + "@value": "Bottled fruit", + "@language": "sw" + }, + { + "@value": "Fruta engarrafada", + "@language": "pt" + }, + { + "@value": "Bottled fruit", + "@language": "oc" + }, + { + "@value": "Бутилированные фрукты", + "@language": "ru" + }, + { + "@value": "Bottled fruit", + "@language": "cy" + }, + { + "@value": "ボトル入りフルーツ", + "@language": "ja" + }, + { + "@value": "Torthaí buidéalaithe", + "@language": "ga" + }, + { + "@value": "बोतलबंद फल", + "@language": "hi" + }, + { + "@value": "Botted成果", + "@language": "zh" + }, + { + "@value": "Bottled fruit", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#processed-fruit", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#pepper", + "rdfs:label": [ + { + "@value": "Poivron", + "@language": "fr" + }, + { + "@value": "Pepper", + "@language": "en" + }, + { + "@value": "Pepper", + "@language": "ar" + }, + { + "@value": "Pepper", + "@language": "ku" + }, + { + "@value": "Pepper", + "@language": "es" + }, + { + "@value": "Pepe", + "@language": "it" + }, + { + "@value": "Pfeffer", + "@language": "de" + }, + { + "@value": "Pepper", + "@language": "sw" + }, + { + "@value": "Pimenta", + "@language": "pt" + }, + { + "@value": "Pepper", + "@language": "oc" + }, + { + "@value": "Пеппер", + "@language": "ru" + }, + { + "@value": "Pepper", + "@language": "cy" + }, + { + "@value": "ペッパー", + "@language": "ja" + }, + { + "@value": "Pepper", + "@language": "ga" + }, + { + "@value": "काली मिर्च", + "@language": "hi" + }, + { + "@value": "A. ep", + "@language": "zh" + }, + { + "@value": "Pepper", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#round-tomato", + "rdfs:label": [ + { + "@value": "Tomate ronde", + "@language": "fr" + }, + { + "@value": "Round tomato", + "@language": "en" + }, + { + "@value": "الطماطم", + "@language": "ar" + }, + { + "@value": "Round tomato", + "@language": "ku" + }, + { + "@value": "Tomate redondo", + "@language": "es" + }, + { + "@value": "Pomodoro rotondo", + "@language": "it" + }, + { + "@value": "Runde Tomaten", + "@language": "de" + }, + { + "@value": "Round tomato", + "@language": "sw" + }, + { + "@value": "Tomate redondo", + "@language": "pt" + }, + { + "@value": "Round tomato", + "@language": "oc" + }, + { + "@value": "Круглый помидор", + "@language": "ru" + }, + { + "@value": "Round tomato", + "@language": "cy" + }, + { + "@value": "ラウンドトマト", + "@language": "ja" + }, + { + "@value": "trátaí cruinn", + "@language": "ga" + }, + { + "@value": "गोल टमाटर", + "@language": "hi" + }, + { + "@value": "马托圆桌会议", + "@language": "zh" + }, + { + "@value": "Round tomato", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#tomato", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#goose", + "rdfs:label": [ + { + "@value": "Oie", + "@language": "fr" + }, + { + "@value": "Goose", + "@language": "en" + }, + { + "@value": "Goose", + "@language": "ar" + }, + { + "@value": "Goose", + "@language": "ku" + }, + { + "@value": "Goose", + "@language": "es" + }, + { + "@value": "Goccia", + "@language": "it" + }, + { + "@value": "Gans", + "@language": "de" + }, + { + "@value": "Goose", + "@language": "sw" + }, + { + "@value": "Ganhe", + "@language": "pt" + }, + { + "@value": "Goose", + "@language": "oc" + }, + { + "@value": "Гусь", + "@language": "ru" + }, + { + "@value": "Goose", + "@language": "cy" + }, + { + "@value": "ゴース", + "@language": "ja" + }, + { + "@value": "Goose", + "@language": "ga" + }, + { + "@value": "गूंज", + "@language": "hi" + }, + { + "@value": "G. 古 巴", + "@language": "zh" + }, + { + "@value": "Goose", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#poultry", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#bottled-vegetable", + "rdfs:label": [ + { + "@value": "Légume en bocal", + "@language": "fr" + }, + { + "@value": "Bottled vegetable", + "@language": "en" + }, + { + "@value": "الخضروات المزروعة", + "@language": "ar" + }, + { + "@value": "Bottled vegetable", + "@language": "ku" + }, + { + "@value": "Verdura embotellada", + "@language": "es" + }, + { + "@value": "Ortaggi in bottiglia", + "@language": "it" + }, + { + "@value": "Gemüse", + "@language": "de" + }, + { + "@value": "Bottled vegetable", + "@language": "sw" + }, + { + "@value": "Vegetal engarrafado", + "@language": "pt" + }, + { + "@value": "Bottled vegetable", + "@language": "oc" + }, + { + "@value": "Бутилированный овощ", + "@language": "ru" + }, + { + "@value": "Bottled vegetable", + "@language": "cy" + }, + { + "@value": "ボトル入り野菜", + "@language": "ja" + }, + { + "@value": "Glasraí buidéal", + "@language": "ga" + }, + { + "@value": "बोतलबंद सब्जी", + "@language": "hi" + }, + { + "@value": "Bottled蔬菜", + "@language": "zh" + }, + { + "@value": "Bottled vegetable", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#processed-vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#cooked-meat", + "rdfs:label": [ + { + "@value": "Viande cuite", + "@language": "fr" + }, + { + "@value": "Cooked meat", + "@language": "en" + }, + { + "@value": "لحم طهي", + "@language": "ar" + }, + { + "@value": "Cooked meat", + "@language": "ku" + }, + { + "@value": "Carne cocida", + "@language": "es" + }, + { + "@value": "Carne cotta", + "@language": "it" + }, + { + "@value": "gekochtes Fleisch", + "@language": "de" + }, + { + "@value": "Cooked meat", + "@language": "sw" + }, + { + "@value": "Carne cozinhada", + "@language": "pt" + }, + { + "@value": "Cooked meat", + "@language": "oc" + }, + { + "@value": "Готовое мясо", + "@language": "ru" + }, + { + "@value": "Cooked meat", + "@language": "cy" + }, + { + "@value": "肉料理", + "@language": "ja" + }, + { + "@value": "Feoil chócaithe", + "@language": "ga" + }, + { + "@value": "पका हुआ मांस", + "@language": "hi" + }, + { + "@value": "A. 质询", + "@language": "zh" + }, + { + "@value": "Cooked meat", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#pork", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "rdfs:label": [ + { + "@value": "Légume", + "@language": "fr" + }, + { + "@value": "Vegetable", + "@language": "en" + }, + { + "@value": "النباتات", + "@language": "ar" + }, + { + "@value": "Vegetable", + "@language": "ku" + }, + { + "@value": "Vegeta", + "@language": "es" + }, + { + "@value": "Ortofrutticoli", + "@language": "it" + }, + { + "@value": "Gemüse", + "@language": "de" + }, + { + "@value": "Vegetable", + "@language": "sw" + }, + { + "@value": "Produtos hortícolas", + "@language": "pt" + }, + { + "@value": "Vegetable", + "@language": "oc" + }, + { + "@value": "Овощи", + "@language": "ru" + }, + { + "@value": "Vegetable", + "@language": "cy" + }, + { + "@value": "サラダ", + "@language": "ja" + }, + { + "@value": "Glasraí Glasraí", + "@language": "ga" + }, + { + "@value": "सब्जी", + "@language": "hi" + }, + { + "@value": "目标", + "@language": "zh" + }, + { + "@value": "Vegetable", + "@language": "ca" + } + ], + "dfc-p:specialize": "undefined", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#quail", + "rdfs:label": [ + { + "@value": "Caille", + "@language": "fr" + }, + { + "@value": "Quail", + "@language": "en" + }, + { + "@value": "Quail", + "@language": "ar" + }, + { + "@value": "Quail", + "@language": "ku" + }, + { + "@value": "Quail", + "@language": "es" + }, + { + "@value": "Quaglia", + "@language": "it" + }, + { + "@value": "Quail", + "@language": "de" + }, + { + "@value": "Quail", + "@language": "sw" + }, + { + "@value": "Qué?", + "@language": "pt" + }, + { + "@value": "Quail", + "@language": "oc" + }, + { + "@value": "Квартира", + "@language": "ru" + }, + { + "@value": "Quail", + "@language": "cy" + }, + { + "@value": "キュール", + "@language": "ja" + }, + { + "@value": "Inis dúinn, le do thoil...", + "@language": "ga" + }, + { + "@value": "क्वैल", + "@language": "hi" + }, + { + "@value": "原告", + "@language": "zh" + }, + { + "@value": "Quail", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#poultry", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#mature-cheese", + "rdfs:label": [ + { + "@value": "Fromage affiné", + "@language": "fr" + }, + { + "@value": "Mature cheese", + "@language": "en" + }, + { + "@value": "الجبنة الطازجة", + "@language": "ar" + }, + { + "@value": "Mature cheese", + "@language": "ku" + }, + { + "@value": "Queso maduro", + "@language": "es" + }, + { + "@value": "Formaggi stagionati", + "@language": "it" + }, + { + "@value": "Milchkäse", + "@language": "de" + }, + { + "@value": "Mature cheese", + "@language": "sw" + }, + { + "@value": "Queijo de milho", + "@language": "pt" + }, + { + "@value": "Mature cheese", + "@language": "oc" + }, + { + "@value": "Зрелый сыр", + "@language": "ru" + }, + { + "@value": "Mature cheese", + "@language": "cy" + }, + { + "@value": "成熟したチーズ", + "@language": "ja" + }, + { + "@value": "Cáis aibí", + "@language": "ga" + }, + { + "@value": "पनीर", + "@language": "hi" + }, + { + "@value": "出生", + "@language": "zh" + }, + { + "@value": "Mature cheese", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#cow-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#lemon", + "rdfs:label": [ + { + "@value": "Citron", + "@language": "fr" + }, + { + "@value": "Lemon", + "@language": "en" + }, + { + "@value": "ليمون", + "@language": "ar" + }, + { + "@value": "Lemon", + "@language": "ku" + }, + { + "@value": "Lemon", + "@language": "es" + }, + { + "@value": "Limone", + "@language": "it" + }, + { + "@value": "Zitronen", + "@language": "de" + }, + { + "@value": "Lemon", + "@language": "sw" + }, + { + "@value": "Limão.", + "@language": "pt" + }, + { + "@value": "Lemon", + "@language": "oc" + }, + { + "@value": "Лимон", + "@language": "ru" + }, + { + "@value": "Lemon", + "@language": "cy" + }, + { + "@value": "レモン", + "@language": "ja" + }, + { + "@value": "Lemon", + "@language": "ga" + }, + { + "@value": "लेमन", + "@language": "hi" + }, + { + "@value": "导 言", + "@language": "zh" + }, + { + "@value": "Lemon", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#fruit", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#pear", + "rdfs:label": [ + { + "@value": "Poire", + "@language": "fr" + }, + { + "@value": "Pear", + "@language": "en" + }, + { + "@value": "الخوف", + "@language": "ar" + }, + { + "@value": "Pear", + "@language": "ku" + }, + { + "@value": "Pear", + "@language": "es" + }, + { + "@value": "Pear", + "@language": "it" + }, + { + "@value": "Birnen", + "@language": "de" + }, + { + "@value": "Pear", + "@language": "sw" + }, + { + "@value": "Pear", + "@language": "pt" + }, + { + "@value": "Pear", + "@language": "oc" + }, + { + "@value": "Груша", + "@language": "ru" + }, + { + "@value": "Pear", + "@language": "cy" + }, + { + "@value": "パア", + "@language": "ja" + }, + { + "@value": "cineál gas: in airde", + "@language": "ga" + }, + { + "@value": "नाशपाती", + "@language": "hi" + }, + { + "@value": "佩尔", + "@language": "zh" + }, + { + "@value": "Pear", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#fruit", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#frozen-fruit", + "rdfs:label": [ + { + "@value": "Fruit surgelé", + "@language": "fr" + }, + { + "@value": "Frozen fruit", + "@language": "en" + }, + { + "@value": "الفاكهة المتجمدة", + "@language": "ar" + }, + { + "@value": "Frozen fruit", + "@language": "ku" + }, + { + "@value": "Fruto congelado", + "@language": "es" + }, + { + "@value": "Frutta congelata", + "@language": "it" + }, + { + "@value": "Gefrorene Früchte", + "@language": "de" + }, + { + "@value": "Frozen fruit", + "@language": "sw" + }, + { + "@value": "Fruta congelada", + "@language": "pt" + }, + { + "@value": "Frozen fruit", + "@language": "oc" + }, + { + "@value": "Замороженные фрукты", + "@language": "ru" + }, + { + "@value": "Frozen fruit", + "@language": "cy" + }, + { + "@value": "冷凍フルーツ", + "@language": "ja" + }, + { + "@value": "Torthaí reoite", + "@language": "ga" + }, + { + "@value": "फ्रोजन फल", + "@language": "hi" + }, + { + "@value": "Frozen果", + "@language": "zh" + }, + { + "@value": "Frozen fruit", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#frozen", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#peas", + "rdfs:label": [ + { + "@value": "Pois", + "@language": "fr" + }, + { + "@value": "Peas", + "@language": "en" + }, + { + "@value": "Peas", + "@language": "ar" + }, + { + "@value": "Peas", + "@language": "ku" + }, + { + "@value": "Peas", + "@language": "es" + }, + { + "@value": "Pesche", + "@language": "it" + }, + { + "@value": "Erbsen", + "@language": "de" + }, + { + "@value": "Peas", + "@language": "sw" + }, + { + "@value": "Peas", + "@language": "pt" + }, + { + "@value": "Peas", + "@language": "oc" + }, + { + "@value": "Пеас", + "@language": "ru" + }, + { + "@value": "Peas", + "@language": "cy" + }, + { + "@value": "ペアス", + "@language": "ja" + }, + { + "@value": "taiseachas aeir: fliuch", + "@language": "ga" + }, + { + "@value": "पेस", + "@language": "hi" + }, + { + "@value": "佩斯", + "@language": "zh" + }, + { + "@value": "Peas", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#dried-vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#salting", + "rdfs:label": [ + { + "@value": "Salaison", + "@language": "fr" + }, + { + "@value": "Salting", + "@language": "en" + }, + { + "@value": "الملح", + "@language": "ar" + }, + { + "@value": "Salting", + "@language": "ku" + }, + { + "@value": "Salting", + "@language": "es" + }, + { + "@value": "Saldatura", + "@language": "it" + }, + { + "@value": "Salz", + "@language": "de" + }, + { + "@value": "Salting", + "@language": "sw" + }, + { + "@value": "Salgando", + "@language": "pt" + }, + { + "@value": "Salting", + "@language": "oc" + }, + { + "@value": "Соль", + "@language": "ru" + }, + { + "@value": "Salting", + "@language": "cy" + }, + { + "@value": "ソルト", + "@language": "ja" + }, + { + "@value": "An tSlánú", + "@language": "ga" + }, + { + "@value": "नमकीन बनाना", + "@language": "hi" + }, + { + "@value": "薪金", + "@language": "zh" + }, + { + "@value": "Salting", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#pork", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#honey", + "rdfs:label": [ + { + "@value": "Miel", + "@language": "fr" + }, + { + "@value": "Honey", + "@language": "en" + }, + { + "@value": "العسل", + "@language": "ar" + }, + { + "@value": "Honey", + "@language": "ku" + }, + { + "@value": "Cariño", + "@language": "es" + }, + { + "@value": "Tesoro", + "@language": "it" + }, + { + "@value": "Honig", + "@language": "de" + }, + { + "@value": "Honey", + "@language": "sw" + }, + { + "@value": "Querida.", + "@language": "pt" + }, + { + "@value": "Honey", + "@language": "oc" + }, + { + "@value": "Мед", + "@language": "ru" + }, + { + "@value": "Honey", + "@language": "cy" + }, + { + "@value": "ハニー", + "@language": "ja" + }, + { + "@value": "Bláthanna faoi dhíon", + "@language": "ga" + }, + { + "@value": "हनी", + "@language": "hi" + }, + { + "@value": "霍尼岛", + "@language": "zh" + }, + { + "@value": "Honey", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#sweet-groceries", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#frozen-meal", + "rdfs:label": [ + { + "@value": "Plat surgelé", + "@language": "fr" + }, + { + "@value": "Frozen meal", + "@language": "en" + }, + { + "@value": "وجبة مجمدة", + "@language": "ar" + }, + { + "@value": "Frozen meal", + "@language": "ku" + }, + { + "@value": "Comida congelado", + "@language": "es" + }, + { + "@value": "Pasto congelato", + "@language": "it" + }, + { + "@value": "Gefrorene Mahlzeiten", + "@language": "de" + }, + { + "@value": "Frozen meal", + "@language": "sw" + }, + { + "@value": "Comida congelada", + "@language": "pt" + }, + { + "@value": "Frozen meal", + "@language": "oc" + }, + { + "@value": "Замороженная еда", + "@language": "ru" + }, + { + "@value": "Frozen meal", + "@language": "cy" + }, + { + "@value": "冷凍食", + "@language": "ja" + }, + { + "@value": "Béile reoite", + "@language": "ga" + }, + { + "@value": "जमे हुए भोजन", + "@language": "hi" + }, + { + "@value": "食堂", + "@language": "zh" + }, + { + "@value": "Frozen meal", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#frozen", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#pasta", + "rdfs:label": [ + { + "@value": "Pâte", + "@language": "fr" + }, + { + "@value": "Pasta", + "@language": "en" + }, + { + "@value": "باستا", + "@language": "ar" + }, + { + "@value": "Pasta", + "@language": "ku" + }, + { + "@value": "Pasta", + "@language": "es" + }, + { + "@value": "Pasta", + "@language": "it" + }, + { + "@value": "Pasta", + "@language": "de" + }, + { + "@value": "Pasta", + "@language": "sw" + }, + { + "@value": "Pasta", + "@language": "pt" + }, + { + "@value": "Pasta", + "@language": "oc" + }, + { + "@value": "Паста", + "@language": "ru" + }, + { + "@value": "Pasta", + "@language": "cy" + }, + { + "@value": "パスタ", + "@language": "ja" + }, + { + "@value": "taiseachas aeir: fliuch", + "@language": "ga" + }, + { + "@value": "पास्ता", + "@language": "hi" + }, + { + "@value": "Pasta", + "@language": "zh" + }, + { + "@value": "Pasta", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#savory-groceries", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#cherry-tomato", + "rdfs:label": [ + { + "@value": "Tomate cerise", + "@language": "fr" + }, + { + "@value": "Cherry tomato", + "@language": "en" + }, + { + "@value": "طماطم الكرز", + "@language": "ar" + }, + { + "@value": "Cherry tomato", + "@language": "ku" + }, + { + "@value": "Tomate cereza", + "@language": "es" + }, + { + "@value": "Pomodoro di ciliegia", + "@language": "it" + }, + { + "@value": "Tomaten", + "@language": "de" + }, + { + "@value": "Cherry tomato", + "@language": "sw" + }, + { + "@value": "Tomate de cereja", + "@language": "pt" + }, + { + "@value": "Cherry tomato", + "@language": "oc" + }, + { + "@value": "Черри помидор", + "@language": "ru" + }, + { + "@value": "Cherry tomato", + "@language": "cy" + }, + { + "@value": "チェリートマト", + "@language": "ja" + }, + { + "@value": "Trátaí Silíní", + "@language": "ga" + }, + { + "@value": "चेरी टमाटर", + "@language": "hi" + }, + { + "@value": "Cherrytomato", + "@language": "zh" + }, + { + "@value": "Cherry tomato", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#tomato", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#frozen-meat", + "rdfs:label": [ + { + "@value": "Viande surgelée", + "@language": "fr" + }, + { + "@value": "Frozen meat", + "@language": "en" + }, + { + "@value": "اللحم المتجمد", + "@language": "ar" + }, + { + "@value": "Frozen meat", + "@language": "ku" + }, + { + "@value": "Carne congelado", + "@language": "es" + }, + { + "@value": "Carne congelata", + "@language": "it" + }, + { + "@value": "gefrorenes Fleisch", + "@language": "de" + }, + { + "@value": "Frozen meat", + "@language": "sw" + }, + { + "@value": "Carne congelada", + "@language": "pt" + }, + { + "@value": "Frozen meat", + "@language": "oc" + }, + { + "@value": "Замороженное мясо", + "@language": "ru" + }, + { + "@value": "Frozen meat", + "@language": "cy" + }, + { + "@value": "冷凍肉", + "@language": "ja" + }, + { + "@value": "feoil reoite", + "@language": "ga" + }, + { + "@value": "जमे हुए मांस", + "@language": "hi" + }, + { + "@value": "Frozen肉", + "@language": "zh" + }, + { + "@value": "Frozen meat", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#frozen", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#non-local-fruit", + "rdfs:label": [ + { + "@value": "Fruit non local", + "@language": "fr" + }, + { + "@value": "Non-local fruit", + "@language": "en" + }, + { + "@value": "الفاكهة غير المحلية", + "@language": "ar" + }, + { + "@value": "Non-local fruit", + "@language": "ku" + }, + { + "@value": "Fruto no local", + "@language": "es" + }, + { + "@value": "Frutta non locale", + "@language": "it" + }, + { + "@value": "Nichtlokale Früchte", + "@language": "de" + }, + { + "@value": "Non-local fruit", + "@language": "sw" + }, + { + "@value": "Fruta não local", + "@language": "pt" + }, + { + "@value": "Non-local fruit", + "@language": "oc" + }, + { + "@value": "Нелокальные фрукты", + "@language": "ru" + }, + { + "@value": "Non-local fruit", + "@language": "cy" + }, + { + "@value": "ノンローカルフルーツ", + "@language": "ja" + }, + { + "@value": "Torthaí neamh-áitiúil", + "@language": "ga" + }, + { + "@value": "गैर स्थानीय फल", + "@language": "hi" + }, + { + "@value": "非当地成果", + "@language": "zh" + }, + { + "@value": "Non-local fruit", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#fruit", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#clementine", + "rdfs:label": [ + { + "@value": "Clémentine", + "@language": "fr" + }, + { + "@value": "Clementine", + "@language": "en" + }, + { + "@value": "Clementine", + "@language": "ar" + }, + { + "@value": "Clementine", + "@language": "ku" + }, + { + "@value": "Clementine", + "@language": "es" + }, + { + "@value": "Clemente", + "@language": "it" + }, + { + "@value": "Clementin", + "@language": "de" + }, + { + "@value": "Clementine", + "@language": "sw" + }, + { + "@value": "Clementine.", + "@language": "pt" + }, + { + "@value": "Clementine", + "@language": "oc" + }, + { + "@value": "Клементин", + "@language": "ru" + }, + { + "@value": "Clementine", + "@language": "cy" + }, + { + "@value": "クレメンテイン", + "@language": "ja" + }, + { + "@value": "cineál gas: in airde", + "@language": "ga" + }, + { + "@value": "क्लेमेंटाइन", + "@language": "hi" + }, + { + "@value": "Clementine", + "@language": "zh" + }, + { + "@value": "Clementine", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#fruit", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#other-milk", + "rdfs:label": [ + { + "@value": "Lait", + "@language": "fr" + }, + { + "@value": "Other Milk", + "@language": "en" + }, + { + "@value": "حليب آخر", + "@language": "ar" + }, + { + "@value": "Other Milk", + "@language": "ku" + }, + { + "@value": "Otras leches", + "@language": "es" + }, + { + "@value": "Altri Latte", + "@language": "it" + }, + { + "@value": "Sonstige Milch", + "@language": "de" + }, + { + "@value": "Other Milk", + "@language": "sw" + }, + { + "@value": "Outros", + "@language": "pt" + }, + { + "@value": "Other Milk", + "@language": "oc" + }, + { + "@value": "Другое молоко", + "@language": "ru" + }, + { + "@value": "Other Milk", + "@language": "cy" + }, + { + "@value": "その他の牛乳", + "@language": "ja" + }, + { + "@value": "Bainne eile", + "@language": "ga" + }, + { + "@value": "अन्य दूध", + "@language": "hi" + }, + { + "@value": "其他 Milk", + "@language": "zh" + }, + { + "@value": "Other Milk", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#other-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#dill", + "rdfs:label": [ + { + "@value": "Aneth", + "@language": "fr" + }, + { + "@value": "Dill", + "@language": "en" + }, + { + "@value": "Dill", + "@language": "ar" + }, + { + "@value": "Dill", + "@language": "ku" + }, + { + "@value": "Dill", + "@language": "es" + }, + { + "@value": "Dillo.", + "@language": "it" + }, + { + "@value": "Dill", + "@language": "de" + }, + { + "@value": "Dill", + "@language": "sw" + }, + { + "@value": "Dill", + "@language": "pt" + }, + { + "@value": "Dill", + "@language": "oc" + }, + { + "@value": "Дилли", + "@language": "ru" + }, + { + "@value": "Dill", + "@language": "cy" + }, + { + "@value": "ディル", + "@language": "ja" + }, + { + "@value": "Dill Dill", + "@language": "ga" + }, + { + "@value": "डिल", + "@language": "hi" + }, + { + "@value": "Dill", + "@language": "zh" + }, + { + "@value": "Dill", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#aromatic", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#lemonade", + "rdfs:label": [ + { + "@value": "Limonade", + "@language": "fr" + }, + { + "@value": "Lemonade", + "@language": "en" + }, + { + "@value": "Lemonade", + "@language": "ar" + }, + { + "@value": "Lemonade", + "@language": "ku" + }, + { + "@value": "Lemonade", + "@language": "es" + }, + { + "@value": "Limonate", + "@language": "it" + }, + { + "@value": "Lemonade", + "@language": "de" + }, + { + "@value": "Lemonade", + "@language": "sw" + }, + { + "@value": "Limão", + "@language": "pt" + }, + { + "@value": "Lemonade", + "@language": "oc" + }, + { + "@value": "Лимонад", + "@language": "ru" + }, + { + "@value": "Lemonade", + "@language": "cy" + }, + { + "@value": "レモネード", + "@language": "ja" + }, + { + "@value": "Lemonade", + "@language": "ga" + }, + { + "@value": "लेमनेड", + "@language": "hi" + }, + { + "@value": "薪酬", + "@language": "zh" + }, + { + "@value": "Lemonade", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#soft-drink", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#rice", + "rdfs:label": [ + { + "@value": "Riz", + "@language": "fr" + }, + { + "@value": "Rice", + "@language": "en" + }, + { + "@value": "Rice", + "@language": "ar" + }, + { + "@value": "Rice", + "@language": "ku" + }, + { + "@value": "Rice", + "@language": "es" + }, + { + "@value": "Riso", + "@language": "it" + }, + { + "@value": "Reis", + "@language": "de" + }, + { + "@value": "Rice", + "@language": "sw" + }, + { + "@value": "Rice", + "@language": "pt" + }, + { + "@value": "Rice", + "@language": "oc" + }, + { + "@value": "Рис", + "@language": "ru" + }, + { + "@value": "Rice", + "@language": "cy" + }, + { + "@value": "ライス", + "@language": "ja" + }, + { + "@value": "Riceáil", + "@language": "ga" + }, + { + "@value": "चावल", + "@language": "hi" + }, + { + "@value": "评 注", + "@language": "zh" + }, + { + "@value": "Rice", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#savory-groceries", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#snails", + "rdfs:label": [ + { + "@value": "Escargots", + "@language": "fr" + }, + { + "@value": "Snails", + "@language": "en" + }, + { + "@value": "الحلزون", + "@language": "ar" + }, + { + "@value": "Snails", + "@language": "ku" + }, + { + "@value": "Caracoles", + "@language": "es" + }, + { + "@value": "Lumache", + "@language": "it" + }, + { + "@value": "Schnecken", + "@language": "de" + }, + { + "@value": "Snails", + "@language": "sw" + }, + { + "@value": "Unhas", + "@language": "pt" + }, + { + "@value": "Snails", + "@language": "oc" + }, + { + "@value": "Снайлы", + "@language": "ru" + }, + { + "@value": "Snails", + "@language": "cy" + }, + { + "@value": "カタツムリ", + "@language": "ja" + }, + { + "@value": "Saillíní", + "@language": "ga" + }, + { + "@value": "घोंघे", + "@language": "hi" + }, + { + "@value": "Snails", + "@language": "zh" + }, + { + "@value": "Snails", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#meat-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#fifth-range-vegetable", + "rdfs:label": [ + { + "@value": "Légume cinquième gamme", + "@language": "fr" + }, + { + "@value": "Fifth range vegetable", + "@language": "en" + }, + { + "@value": "المجموعة الخامسة", + "@language": "ar" + }, + { + "@value": "Fifth range vegetable", + "@language": "ku" + }, + { + "@value": "Quinta gama de verduras", + "@language": "es" + }, + { + "@value": "Ortaggio della quinta gamma", + "@language": "it" + }, + { + "@value": "Fünfte Range Gemüse", + "@language": "de" + }, + { + "@value": "Fifth range vegetable", + "@language": "sw" + }, + { + "@value": "Quinta gama de vegetais", + "@language": "pt" + }, + { + "@value": "Fifth range vegetable", + "@language": "oc" + }, + { + "@value": "Пятый ассортимент овощ", + "@language": "ru" + }, + { + "@value": "Fifth range vegetable", + "@language": "cy" + }, + { + "@value": "第5レンジ野菜", + "@language": "ja" + }, + { + "@value": "Cúigiú raon glasraí", + "@language": "ga" + }, + { + "@value": "पांचवी रेंज सब्जी", + "@language": "hi" + }, + { + "@value": "第五种蔬菜", + "@language": "zh" + }, + { + "@value": "Fifth range vegetable", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#turnip", + "rdfs:label": [ + { + "@value": "Navet", + "@language": "fr" + }, + { + "@value": "Turnip", + "@language": "en" + }, + { + "@value": "Turnip", + "@language": "ar" + }, + { + "@value": "Turnip", + "@language": "ku" + }, + { + "@value": "Turnip", + "@language": "es" + }, + { + "@value": "Turnip", + "@language": "it" + }, + { + "@value": "Keks", + "@language": "de" + }, + { + "@value": "Turnip", + "@language": "sw" + }, + { + "@value": "Toca a mexer.", + "@language": "pt" + }, + { + "@value": "Turnip", + "@language": "oc" + }, + { + "@value": "Рецепт", + "@language": "ru" + }, + { + "@value": "Turnip", + "@language": "cy" + }, + { + "@value": "ログイン", + "@language": "ja" + }, + { + "@value": "taiseachas aeir: fliuch", + "@language": "ga" + }, + { + "@value": "टर्निप", + "@language": "hi" + }, + { + "@value": "导 言", + "@language": "zh" + }, + { + "@value": "Turnip", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#cluster-tomato", + "rdfs:label": [ + { + "@value": "Tomate grappe", + "@language": "fr" + }, + { + "@value": "Cluster tomato", + "@language": "en" + }, + { + "@value": "طماطم المجموعة", + "@language": "ar" + }, + { + "@value": "Cluster tomato", + "@language": "ku" + }, + { + "@value": "Tomate de racimo", + "@language": "es" + }, + { + "@value": "Pomodoro di cluster", + "@language": "it" + }, + { + "@value": "Gesunde Tomaten", + "@language": "de" + }, + { + "@value": "Cluster tomato", + "@language": "sw" + }, + { + "@value": "Tomate de pasta", + "@language": "pt" + }, + { + "@value": "Cluster tomato", + "@language": "oc" + }, + { + "@value": "Кластер томат", + "@language": "ru" + }, + { + "@value": "Cluster tomato", + "@language": "cy" + }, + { + "@value": "クラスタートマト", + "@language": "ja" + }, + { + "@value": "Trátaí caol", + "@language": "ga" + }, + { + "@value": "क्लस्टर टमाटर", + "@language": "hi" + }, + { + "@value": "马托组", + "@language": "zh" + }, + { + "@value": "Cluster tomato", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#tomato", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#cabbage", + "rdfs:label": [ + { + "@value": "Chou", + "@language": "fr" + }, + { + "@value": "Cabbage", + "@language": "en" + }, + { + "@value": "Cabbage", + "@language": "ar" + }, + { + "@value": "Cabbage", + "@language": "ku" + }, + { + "@value": "Cabbage", + "@language": "es" + }, + { + "@value": "Cabbage", + "@language": "it" + }, + { + "@value": "Kohl", + "@language": "de" + }, + { + "@value": "Cabbage", + "@language": "sw" + }, + { + "@value": "Repolho", + "@language": "pt" + }, + { + "@value": "Cabbage", + "@language": "oc" + }, + { + "@value": "Каюта", + "@language": "ru" + }, + { + "@value": "Cabbage", + "@language": "cy" + }, + { + "@value": "キャベツ", + "@language": "ja" + }, + { + "@value": "Cabáiste", + "@language": "ga" + }, + { + "@value": "गोभी", + "@language": "hi" + }, + { + "@value": "卡宾", + "@language": "zh" + }, + { + "@value": "Cabbage", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#fruit-in-compote", + "rdfs:label": [ + { + "@value": "Fruit en compote", + "@language": "fr" + }, + { + "@value": "Fruit in compote", + "@language": "en" + }, + { + "@value": "فراغ في التأقلم", + "@language": "ar" + }, + { + "@value": "Fruit in compote", + "@language": "ku" + }, + { + "@value": "Frutas en compota", + "@language": "es" + }, + { + "@value": "Frutta in composta", + "@language": "it" + }, + { + "@value": "Frucht in Kompott", + "@language": "de" + }, + { + "@value": "Fruit in compote", + "@language": "sw" + }, + { + "@value": "Frutas em compota", + "@language": "pt" + }, + { + "@value": "Fruit in compote", + "@language": "oc" + }, + { + "@value": "Фрукты в компоте", + "@language": "ru" + }, + { + "@value": "Fruit in compote", + "@language": "cy" + }, + { + "@value": "コンポートの果実", + "@language": "ja" + }, + { + "@value": "Torthaí i compote", + "@language": "ga" + }, + { + "@value": "खाद में फल", + "@language": "hi" + }, + { + "@value": "意大利的弗朗西", + "@language": "zh" + }, + { + "@value": "Fruit in compote", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#processed-fruit", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#digestive", + "rdfs:label": [ + { + "@value": "Digestif", + "@language": "fr" + }, + { + "@value": "Digestive", + "@language": "en" + }, + { + "@value": "Digestive", + "@language": "ar" + }, + { + "@value": "Digestive", + "@language": "ku" + }, + { + "@value": "Digestivo", + "@language": "es" + }, + { + "@value": "Digestivo", + "@language": "it" + }, + { + "@value": "Verdauungsstörungen", + "@language": "de" + }, + { + "@value": "Digestive", + "@language": "sw" + }, + { + "@value": "Digestão", + "@language": "pt" + }, + { + "@value": "Digestive", + "@language": "oc" + }, + { + "@value": "Дайджест", + "@language": "ru" + }, + { + "@value": "Digestive", + "@language": "cy" + }, + { + "@value": "ダイジェスト", + "@language": "ja" + }, + { + "@value": "Díroghnaigh gach rud", + "@language": "ga" + }, + { + "@value": "पाचन", + "@language": "hi" + }, + { + "@value": "压力", + "@language": "zh" + }, + { + "@value": "Digestive", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#alcoholic-beverage", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#truffle", + "rdfs:label": [ + { + "@value": "Truffe", + "@language": "fr" + }, + { + "@value": "Truffle", + "@language": "en" + }, + { + "@value": "Truffle", + "@language": "ar" + }, + { + "@value": "Truffle", + "@language": "ku" + }, + { + "@value": "Trufa", + "@language": "es" + }, + { + "@value": "Tartufo", + "@language": "it" + }, + { + "@value": "Trüffel", + "@language": "de" + }, + { + "@value": "Truffle", + "@language": "sw" + }, + { + "@value": "Trufa", + "@language": "pt" + }, + { + "@value": "Truffle", + "@language": "oc" + }, + { + "@value": "Трюфель", + "@language": "ru" + }, + { + "@value": "Truffle", + "@language": "cy" + }, + { + "@value": "トリュフ", + "@language": "ja" + }, + { + "@value": "Toir agus Crainn", + "@language": "ga" + }, + { + "@value": "ट्रफल", + "@language": "hi" + }, + { + "@value": "钢盔", + "@language": "zh" + }, + { + "@value": "Truffle", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#mushroom", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#goat-yogurt-on-a-bed-of-fruit", + "rdfs:label": [ + { + "@value": "Yaourt sur lit de fruit", + "@language": "fr" + }, + { + "@value": "Goat yogurt on a bed of fruit", + "@language": "en" + }, + { + "@value": "زبادي على سرير الفاكهة", + "@language": "ar" + }, + { + "@value": "Goat yogurt on a bed of fruit", + "@language": "ku" + }, + { + "@value": "Yogur de cabra en una cama de fruta", + "@language": "es" + }, + { + "@value": "Yogurt di capra su un letto di frutta", + "@language": "it" + }, + { + "@value": "Ziegenjoghurt auf einem Bett mit Obst", + "@language": "de" + }, + { + "@value": "Goat yogurt on a bed of fruit", + "@language": "sw" + }, + { + "@value": "iogurte de cabra em uma cama de frutas", + "@language": "pt" + }, + { + "@value": "Goat yogurt on a bed of fruit", + "@language": "oc" + }, + { + "@value": "Goat yogurt на кровати фруктов", + "@language": "ru" + }, + { + "@value": "Goat yogurt on a bed of fruit", + "@language": "cy" + }, + { + "@value": "フルーツのベッドにヤギヨーグルトを食べる", + "@language": "ja" + }, + { + "@value": "Iógart goat ar leaba torthaí", + "@language": "ga" + }, + { + "@value": "एक बिस्तर पर बकरी दही", + "@language": "hi" + }, + { + "@value": "Gatyogurt on a result", + "@language": "zh" + }, + { + "@value": "Goat yogurt on a bed of fruit", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#goat-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#flour", + "rdfs:label": [ + { + "@value": "Farine", + "@language": "fr" + }, + { + "@value": "Flour", + "@language": "en" + }, + { + "@value": "الفيضانات", + "@language": "ar" + }, + { + "@value": "Flour", + "@language": "ku" + }, + { + "@value": "Flour", + "@language": "es" + }, + { + "@value": "Flour", + "@language": "it" + }, + { + "@value": "Mehl", + "@language": "de" + }, + { + "@value": "Flour", + "@language": "sw" + }, + { + "@value": "Farinha", + "@language": "pt" + }, + { + "@value": "Flour", + "@language": "oc" + }, + { + "@value": "Мука", + "@language": "ru" + }, + { + "@value": "Flour", + "@language": "cy" + }, + { + "@value": "フローリング", + "@language": "ja" + }, + { + "@value": "bláthanna cumhra: cumhráin", + "@language": "ga" + }, + { + "@value": "आटा", + "@language": "hi" + }, + { + "@value": "Flour", + "@language": "zh" + }, + { + "@value": "Flour", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#savory-groceries", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#eggplant", + "rdfs:label": [ + { + "@value": "Aubergine", + "@language": "fr" + }, + { + "@value": "Eggplant", + "@language": "en" + }, + { + "@value": "Eggplant", + "@language": "ar" + }, + { + "@value": "Eggplant", + "@language": "ku" + }, + { + "@value": "Eggplant", + "@language": "es" + }, + { + "@value": "melanzane", + "@language": "it" + }, + { + "@value": "Auberginen", + "@language": "de" + }, + { + "@value": "Eggplant", + "@language": "sw" + }, + { + "@value": "Planta de ovo", + "@language": "pt" + }, + { + "@value": "Eggplant", + "@language": "oc" + }, + { + "@value": "Яйцоплант", + "@language": "ru" + }, + { + "@value": "Eggplant", + "@language": "cy" + }, + { + "@value": "エッグプラント", + "@language": "ja" + }, + { + "@value": "taiseachas aeir: fliuch", + "@language": "ga" + }, + { + "@value": "बैंगन", + "@language": "hi" + }, + { + "@value": "植树造林", + "@language": "zh" + }, + { + "@value": "Eggplant", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#cress", + "rdfs:label": [ + { + "@value": "Cresson", + "@language": "fr" + }, + { + "@value": "Cress", + "@language": "en" + }, + { + "@value": "الكرز", + "@language": "ar" + }, + { + "@value": "Cress", + "@language": "ku" + }, + { + "@value": "Cress", + "@language": "es" + }, + { + "@value": "Cress", + "@language": "it" + }, + { + "@value": "Cres", + "@language": "de" + }, + { + "@value": "Cress", + "@language": "sw" + }, + { + "@value": "Cresci", + "@language": "pt" + }, + { + "@value": "Cress", + "@language": "oc" + }, + { + "@value": "Крепость", + "@language": "ru" + }, + { + "@value": "Cress", + "@language": "cy" + }, + { + "@value": "クレッツ", + "@language": "ja" + }, + { + "@value": "Aisling", + "@language": "ga" + }, + { + "@value": "क्रिस", + "@language": "hi" + }, + { + "@value": "Cress", + "@language": "zh" + }, + { + "@value": "Cress", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#salad", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#quince", + "rdfs:label": [ + { + "@value": "Coing", + "@language": "fr" + }, + { + "@value": "Quince", + "@language": "en" + }, + { + "@value": "Quince", + "@language": "ar" + }, + { + "@value": "Quince", + "@language": "ku" + }, + { + "@value": "Quince", + "@language": "es" + }, + { + "@value": "Quince", + "@language": "it" + }, + { + "@value": "Quitten", + "@language": "de" + }, + { + "@value": "Quince", + "@language": "sw" + }, + { + "@value": "Quince", + "@language": "pt" + }, + { + "@value": "Quince", + "@language": "oc" + }, + { + "@value": "Тишина", + "@language": "ru" + }, + { + "@value": "Quince", + "@language": "cy" + }, + { + "@value": "クインス", + "@language": "ja" + }, + { + "@value": "cineál gas: in airde", + "@language": "ga" + }, + { + "@value": "क्विन", + "@language": "hi" + }, + { + "@value": "基 基 斯", + "@language": "zh" + }, + { + "@value": "Quince", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#fruit", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#sweet-yogurt", + "rdfs:label": [ + { + "@value": "Yaourt sucré", + "@language": "fr" + }, + { + "@value": "Sweet yogurt", + "@language": "en" + }, + { + "@value": "الزبادي الحلو", + "@language": "ar" + }, + { + "@value": "Sweet yogurt", + "@language": "ku" + }, + { + "@value": "Dulce yogur", + "@language": "es" + }, + { + "@value": "yogurt dolce", + "@language": "it" + }, + { + "@value": "Süßer Joghurt", + "@language": "de" + }, + { + "@value": "Sweet yogurt", + "@language": "sw" + }, + { + "@value": "Iogurte doce", + "@language": "pt" + }, + { + "@value": "Sweet yogurt", + "@language": "oc" + }, + { + "@value": "Сладкий йогурт", + "@language": "ru" + }, + { + "@value": "Sweet yogurt", + "@language": "cy" + }, + { + "@value": "甘いヨーグルト", + "@language": "ja" + }, + { + "@value": "iógart milis", + "@language": "ga" + }, + { + "@value": "मीठा दही", + "@language": "hi" + }, + { + "@value": "Sweet yogurt", + "@language": "zh" + }, + { + "@value": "Sweet yogurt", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#cow-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#laurel", + "rdfs:label": [ + { + "@value": "Laurier", + "@language": "fr" + }, + { + "@value": "Laurel", + "@language": "en" + }, + { + "@value": "لوريل", + "@language": "ar" + }, + { + "@value": "Laurel", + "@language": "ku" + }, + { + "@value": "Laurel", + "@language": "es" + }, + { + "@value": "Laurel.", + "@language": "it" + }, + { + "@value": "Lorbeer", + "@language": "de" + }, + { + "@value": "Laurel", + "@language": "sw" + }, + { + "@value": "Laurel.", + "@language": "pt" + }, + { + "@value": "Laurel", + "@language": "oc" + }, + { + "@value": "Лавр", + "@language": "ru" + }, + { + "@value": "Laurel", + "@language": "cy" + }, + { + "@value": "ローレル", + "@language": "ja" + }, + { + "@value": "taiseachas aeir: fliuch", + "@language": "ga" + }, + { + "@value": "लॉरेल", + "@language": "hi" + }, + { + "@value": "Lurel", + "@language": "zh" + }, + { + "@value": "Laurel", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#aromatic", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#sheep-dairy-dessert", + "rdfs:label": [ + { + "@value": "Desserts lactés", + "@language": "fr" + }, + { + "@value": "Sheep Dairy dessert", + "@language": "en" + }, + { + "@value": "حلوى الحلويات", + "@language": "ar" + }, + { + "@value": "Sheep Dairy dessert", + "@language": "ku" + }, + { + "@value": "Postre lácteo de ovejas", + "@language": "es" + }, + { + "@value": "Dessert di latticini di pecore", + "@language": "it" + }, + { + "@value": "Sheep Dairy Dessert", + "@language": "de" + }, + { + "@value": "Sheep Dairy dessert", + "@language": "sw" + }, + { + "@value": "Sobremesa de carneiro", + "@language": "pt" + }, + { + "@value": "Sheep Dairy dessert", + "@language": "oc" + }, + { + "@value": "Овцы Молочный десерт", + "@language": "ru" + }, + { + "@value": "Sheep Dairy dessert", + "@language": "cy" + }, + { + "@value": "羊の酪農場デザート", + "@language": "ja" + }, + { + "@value": "Milseog Déiríochta Caorach", + "@language": "ga" + }, + { + "@value": "भेड़ डेयरी मिठाई", + "@language": "hi" + }, + { + "@value": "她", + "@language": "zh" + }, + { + "@value": "Sheep Dairy dessert", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#sheep-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#raspberry", + "rdfs:label": [ + { + "@value": "Framboise", + "@language": "fr" + }, + { + "@value": "Raspberry", + "@language": "en" + }, + { + "@value": "فراولة", + "@language": "ar" + }, + { + "@value": "Raspberry", + "@language": "ku" + }, + { + "@value": "Raspberry", + "@language": "es" + }, + { + "@value": "lampone", + "@language": "it" + }, + { + "@value": "Himbeer", + "@language": "de" + }, + { + "@value": "Raspberry", + "@language": "sw" + }, + { + "@value": "Framboesa", + "@language": "pt" + }, + { + "@value": "Raspberry", + "@language": "oc" + }, + { + "@value": "малина", + "@language": "ru" + }, + { + "@value": "Raspberry", + "@language": "cy" + }, + { + "@value": "ラズベリー", + "@language": "ja" + }, + { + "@value": "cineál gas: in airde", + "@language": "ga" + }, + { + "@value": "रास्पबेरी", + "@language": "hi" + }, + { + "@value": "Raspberry", + "@language": "zh" + }, + { + "@value": "Raspberry", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#berry", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#hazelnut", + "rdfs:label": [ + { + "@value": "Noisette", + "@language": "fr" + }, + { + "@value": "Hazelnut", + "@language": "en" + }, + { + "@value": "Hazelnut", + "@language": "ar" + }, + { + "@value": "Hazelnut", + "@language": "ku" + }, + { + "@value": "Hazelnut", + "@language": "es" + }, + { + "@value": "Nocciola", + "@language": "it" + }, + { + "@value": "Hazeln", + "@language": "de" + }, + { + "@value": "Hazelnut", + "@language": "sw" + }, + { + "@value": "Avelã", + "@language": "pt" + }, + { + "@value": "Hazelnut", + "@language": "oc" + }, + { + "@value": "Карий", + "@language": "ru" + }, + { + "@value": "Hazelnut", + "@language": "cy" + }, + { + "@value": "ヘーゼルナッツ", + "@language": "ja" + }, + { + "@value": "cliceáil grianghraf a mhéadú", + "@language": "ga" + }, + { + "@value": "अखरोट", + "@language": "hi" + }, + { + "@value": "Hazelnut", + "@language": "zh" + }, + { + "@value": "Hazelnut", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#nut", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#delicatessen", + "rdfs:label": [ + { + "@value": "Charcuterie", + "@language": "fr" + }, + { + "@value": "Delicatessen", + "@language": "en" + }, + { + "@value": "Delicatessen", + "@language": "ar" + }, + { + "@value": "Delicatessen", + "@language": "ku" + }, + { + "@value": "Delicatessen", + "@language": "es" + }, + { + "@value": "Delicazioni", + "@language": "it" + }, + { + "@value": "In den Warenkorb", + "@language": "de" + }, + { + "@value": "Delicatessen", + "@language": "sw" + }, + { + "@value": "Aperitivos", + "@language": "pt" + }, + { + "@value": "Delicatessen", + "@language": "oc" + }, + { + "@value": "Деликатыsen", + "@language": "ru" + }, + { + "@value": "Delicatessen", + "@language": "cy" + }, + { + "@value": "デリカテッセン", + "@language": "ja" + }, + { + "@value": "Toir agus Crainn bláthanna", + "@language": "ga" + }, + { + "@value": "Delicatessen", + "@language": "hi" + }, + { + "@value": "Delicsen", + "@language": "zh" + }, + { + "@value": "Delicatessen", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#pork", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#sheep-natural-yogurt", + "rdfs:label": [ + { + "@value": "Yaourt nature", + "@language": "fr" + }, + { + "@value": "Sheep natural yogurt", + "@language": "en" + }, + { + "@value": "الزبادي الطبيعي", + "@language": "ar" + }, + { + "@value": "Sheep natural yogurt", + "@language": "ku" + }, + { + "@value": "Oveja yogur natural", + "@language": "es" + }, + { + "@value": "Yogurt naturale di pecore", + "@language": "it" + }, + { + "@value": "Schafe natürlicher Joghurt", + "@language": "de" + }, + { + "@value": "Sheep natural yogurt", + "@language": "sw" + }, + { + "@value": "Iogurte natural de ovelhas", + "@language": "pt" + }, + { + "@value": "Sheep natural yogurt", + "@language": "oc" + }, + { + "@value": "Овцы натуральные йогурты", + "@language": "ru" + }, + { + "@value": "Sheep natural yogurt", + "@language": "cy" + }, + { + "@value": "羊の天然ヨーグルト", + "@language": "ja" + }, + { + "@value": "Iógart nádúrtha a chothú", + "@language": "ga" + }, + { + "@value": "भेड़ प्राकृतिक दही", + "@language": "hi" + }, + { + "@value": "她承认自然植物群", + "@language": "zh" + }, + { + "@value": "Sheep natural yogurt", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#sheep-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#bakery", + "rdfs:label": [ + { + "@value": "Boulangerie", + "@language": "fr" + }, + { + "@value": "Bakery", + "@language": "en" + }, + { + "@value": "بيكري", + "@language": "ar" + }, + { + "@value": "Bakery", + "@language": "ku" + }, + { + "@value": "Panadería", + "@language": "es" + }, + { + "@value": "Panificio", + "@language": "it" + }, + { + "@value": "Backwaren", + "@language": "de" + }, + { + "@value": "Bakery", + "@language": "sw" + }, + { + "@value": "Padaria", + "@language": "pt" + }, + { + "@value": "Bakery", + "@language": "oc" + }, + { + "@value": "Печенье", + "@language": "ru" + }, + { + "@value": "Bakery", + "@language": "cy" + }, + { + "@value": "ベーカリー", + "@language": "ja" + }, + { + "@value": "Baker na", + "@language": "ga" + }, + { + "@value": "बेकरी", + "@language": "hi" + }, + { + "@value": "B. 巴克托", + "@language": "zh" + }, + { + "@value": "Bakery", + "@language": "ca" + } + ], + "dfc-p:specialize": "undefined", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#lettuce", + "rdfs:label": [ + { + "@value": "Laitue", + "@language": "fr" + }, + { + "@value": "Lettuce", + "@language": "en" + }, + { + "@value": "Lettuce", + "@language": "ar" + }, + { + "@value": "Lettuce", + "@language": "ku" + }, + { + "@value": "Lechuga", + "@language": "es" + }, + { + "@value": "Lattuga", + "@language": "it" + }, + { + "@value": "Salat", + "@language": "de" + }, + { + "@value": "Lettuce", + "@language": "sw" + }, + { + "@value": "alface", + "@language": "pt" + }, + { + "@value": "Lettuce", + "@language": "oc" + }, + { + "@value": "салат", + "@language": "ru" + }, + { + "@value": "Lettuce", + "@language": "cy" + }, + { + "@value": "レタス", + "@language": "ja" + }, + { + "@value": "Déan teagmháil linn", + "@language": "ga" + }, + { + "@value": "सलाद", + "@language": "hi" + }, + { + "@value": "让", + "@language": "zh" + }, + { + "@value": "Lettuce", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#salad", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#veal", + "rdfs:label": [ + { + "@value": "Veau", + "@language": "fr" + }, + { + "@value": "Veal", + "@language": "en" + }, + { + "@value": "فيل", + "@language": "ar" + }, + { + "@value": "Veal", + "@language": "ku" + }, + { + "@value": "Velo", + "@language": "es" + }, + { + "@value": "Veal", + "@language": "it" + }, + { + "@value": "Gemüse", + "@language": "de" + }, + { + "@value": "Veal", + "@language": "sw" + }, + { + "@value": "Veia", + "@language": "pt" + }, + { + "@value": "Veal", + "@language": "oc" + }, + { + "@value": "Веаль", + "@language": "ru" + }, + { + "@value": "Veal", + "@language": "cy" + }, + { + "@value": "ヴェール", + "@language": "ja" + }, + { + "@value": "taiseachas aeir: fliuch", + "@language": "ga" + }, + { + "@value": "वीणा", + "@language": "hi" + }, + { + "@value": "第五十三条", + "@language": "zh" + }, + { + "@value": "Veal", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#meat-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#drink", + "rdfs:label": [ + { + "@value": "Boisson", + "@language": "fr" + }, + { + "@value": "Drink", + "@language": "en" + }, + { + "@value": "اشرب", + "@language": "ar" + }, + { + "@value": "Drink", + "@language": "ku" + }, + { + "@value": "Bebe.", + "@language": "es" + }, + { + "@value": "Bevande", + "@language": "it" + }, + { + "@value": "Getränke", + "@language": "de" + }, + { + "@value": "Drink", + "@language": "sw" + }, + { + "@value": "Bebe.", + "@language": "pt" + }, + { + "@value": "Drink", + "@language": "oc" + }, + { + "@value": "Напитки", + "@language": "ru" + }, + { + "@value": "Drink", + "@language": "cy" + }, + { + "@value": "ドリンク", + "@language": "ja" + }, + { + "@value": "Díroghnaigh gach rud", + "@language": "ga" + }, + { + "@value": "पेय", + "@language": "hi" + }, + { + "@value": "水", + "@language": "zh" + }, + { + "@value": "Drink", + "@language": "ca" + } + ], + "dfc-p:specialize": "undefined", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#tarragon", + "rdfs:label": [ + { + "@value": "Estragon", + "@language": "fr" + }, + { + "@value": "Tarragon", + "@language": "en" + }, + { + "@value": "تاراجون", + "@language": "ar" + }, + { + "@value": "Tarragon", + "@language": "ku" + }, + { + "@value": "Tarragon", + "@language": "es" + }, + { + "@value": "Tarragona", + "@language": "it" + }, + { + "@value": "Estragon", + "@language": "de" + }, + { + "@value": "Tarragon", + "@language": "sw" + }, + { + "@value": "Tarragonismo", + "@language": "pt" + }, + { + "@value": "Tarragon", + "@language": "oc" + }, + { + "@value": "Таррагон", + "@language": "ru" + }, + { + "@value": "Tarragon", + "@language": "cy" + }, + { + "@value": "タラゴン", + "@language": "ja" + }, + { + "@value": "Tarragon", + "@language": "ga" + }, + { + "@value": "तारागोन", + "@language": "hi" + }, + { + "@value": "塔里韦纳", + "@language": "zh" + }, + { + "@value": "Tarragon", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#aromatic", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#meat-product", + "rdfs:label": [ + { + "@value": "Produit carné", + "@language": "fr" + }, + { + "@value": "Meat product", + "@language": "en" + }, + { + "@value": "مُنتَج المِنح", + "@language": "ar" + }, + { + "@value": "Meat product", + "@language": "ku" + }, + { + "@value": "Producto de carne", + "@language": "es" + }, + { + "@value": "Prodotti della carne", + "@language": "it" + }, + { + "@value": "Fleischerzeugnisse", + "@language": "de" + }, + { + "@value": "Meat product", + "@language": "sw" + }, + { + "@value": "Produto de carne", + "@language": "pt" + }, + { + "@value": "Meat product", + "@language": "oc" + }, + { + "@value": "Мясный продукт", + "@language": "ru" + }, + { + "@value": "Meat product", + "@language": "cy" + }, + { + "@value": "肉製品", + "@language": "ja" + }, + { + "@value": "Táirge feola", + "@language": "ga" + }, + { + "@value": "मांस उत्पाद", + "@language": "hi" + }, + { + "@value": "马特产品", + "@language": "zh" + }, + { + "@value": "Meat product", + "@language": "ca" + } + ], + "dfc-p:specialize": "undefined", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#natural-yogurt", + "rdfs:label": [ + { + "@value": "Yaourt nature", + "@language": "fr" + }, + { + "@value": "Natural yogurt", + "@language": "en" + }, + { + "@value": "الزبادي الطبيعي", + "@language": "ar" + }, + { + "@value": "Natural yogurt", + "@language": "ku" + }, + { + "@value": "Yogur natural", + "@language": "es" + }, + { + "@value": "Yogurt naturale", + "@language": "it" + }, + { + "@value": "Natürlicher Joghurt", + "@language": "de" + }, + { + "@value": "Natural yogurt", + "@language": "sw" + }, + { + "@value": "Iogurte natural", + "@language": "pt" + }, + { + "@value": "Natural yogurt", + "@language": "oc" + }, + { + "@value": "Естественный йогурт", + "@language": "ru" + }, + { + "@value": "Natural yogurt", + "@language": "cy" + }, + { + "@value": "天然ヨーグルト", + "@language": "ja" + }, + { + "@value": "iógart nádúrtha", + "@language": "ga" + }, + { + "@value": "प्राकृतिक दही", + "@language": "hi" + }, + { + "@value": "自然污染", + "@language": "zh" + }, + { + "@value": "Natural yogurt", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#cow-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#goat-dairy-dessert", + "rdfs:label": [ + { + "@value": "Dessert lacté", + "@language": "fr" + }, + { + "@value": "Goat Dairy dessert", + "@language": "en" + }, + { + "@value": "حلويات الجو", + "@language": "ar" + }, + { + "@value": "Goat Dairy dessert", + "@language": "ku" + }, + { + "@value": "Pastel de leche", + "@language": "es" + }, + { + "@value": "Dessert di latticini di capra", + "@language": "it" + }, + { + "@value": "Goat Dairy Dessert", + "@language": "de" + }, + { + "@value": "Goat Dairy dessert", + "@language": "sw" + }, + { + "@value": "Sobremesa de carne de vaca", + "@language": "pt" + }, + { + "@value": "Goat Dairy dessert", + "@language": "oc" + }, + { + "@value": "Говядина молочный десерт", + "@language": "ru" + }, + { + "@value": "Goat Dairy dessert", + "@language": "cy" + }, + { + "@value": "ゴート・デイリーデザート", + "@language": "ja" + }, + { + "@value": "Milseog Déiríochta Goat", + "@language": "ga" + }, + { + "@value": "बकरी डेयरी मिठाई", + "@language": "hi" + }, + { + "@value": "Gat Dairy desert", + "@language": "zh" + }, + { + "@value": "Goat Dairy dessert", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#goat-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#milky-mushroom", + "rdfs:label": [ + { + "@value": "Lactaire", + "@language": "fr" + }, + { + "@value": "Milky mushroom", + "@language": "en" + }, + { + "@value": "فطر حليب", + "@language": "ar" + }, + { + "@value": "Milky mushroom", + "@language": "ku" + }, + { + "@value": "Seta leve", + "@language": "es" + }, + { + "@value": "Fungo al latte", + "@language": "it" + }, + { + "@value": "Milchpilze", + "@language": "de" + }, + { + "@value": "Milky mushroom", + "@language": "sw" + }, + { + "@value": "Cogumelo leiteiro", + "@language": "pt" + }, + { + "@value": "Milky mushroom", + "@language": "oc" + }, + { + "@value": "Молочный гриб", + "@language": "ru" + }, + { + "@value": "Milky mushroom", + "@language": "cy" + }, + { + "@value": "乳白色のきのこ", + "@language": "ja" + }, + { + "@value": "Beacán bainne", + "@language": "ga" + }, + { + "@value": "मिल्की मशरूम", + "@language": "hi" + }, + { + "@value": "Milky mush", + "@language": "zh" + }, + { + "@value": "Milky mushroom", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#mushroom", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#cherry", + "rdfs:label": [ + { + "@value": "Cerise", + "@language": "fr" + }, + { + "@value": "Cherry", + "@language": "en" + }, + { + "@value": "Cherry", + "@language": "ar" + }, + { + "@value": "Cherry", + "@language": "ku" + }, + { + "@value": "Cherry", + "@language": "es" + }, + { + "@value": "Cherry", + "@language": "it" + }, + { + "@value": "Kirschen", + "@language": "de" + }, + { + "@value": "Cherry", + "@language": "sw" + }, + { + "@value": "Cereja", + "@language": "pt" + }, + { + "@value": "Cherry", + "@language": "oc" + }, + { + "@value": "Черри", + "@language": "ru" + }, + { + "@value": "Cherry", + "@language": "cy" + }, + { + "@value": "チェリー", + "@language": "ja" + }, + { + "@value": "An tIarthar", + "@language": "ga" + }, + { + "@value": "चेरी", + "@language": "hi" + }, + { + "@value": "Cherry", + "@language": "zh" + }, + { + "@value": "Cherry", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#fruit", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#flavored-yogurt", + "rdfs:label": [ + { + "@value": "Yaourt aromatisé", + "@language": "fr" + }, + { + "@value": "Flavored yogurt", + "@language": "en" + }, + { + "@value": "الزبادي المشتعل", + "@language": "ar" + }, + { + "@value": "Flavored yogurt", + "@language": "ku" + }, + { + "@value": "Yogur de sabor", + "@language": "es" + }, + { + "@value": "Yogurt aromatizzato", + "@language": "it" + }, + { + "@value": "Aromater Joghurt", + "@language": "de" + }, + { + "@value": "Flavored yogurt", + "@language": "sw" + }, + { + "@value": "iogurte saboroso", + "@language": "pt" + }, + { + "@value": "Flavored yogurt", + "@language": "oc" + }, + { + "@value": "Вкусные йогурты", + "@language": "ru" + }, + { + "@value": "Flavored yogurt", + "@language": "cy" + }, + { + "@value": "風味のヨーグルト", + "@language": "ja" + }, + { + "@value": "iógart sclábhaithe", + "@language": "ga" + }, + { + "@value": "स्वादयुक्त दही", + "@language": "hi" + }, + { + "@value": "Flavor yogurt", + "@language": "zh" + }, + { + "@value": "Flavored yogurt", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#cow-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#squash-melon", + "rdfs:label": [ + { + "@value": "Pâtisson", + "@language": "fr" + }, + { + "@value": "Squash melon", + "@language": "en" + }, + { + "@value": "البطيخ", + "@language": "ar" + }, + { + "@value": "Squash melon", + "@language": "ku" + }, + { + "@value": "Squash melon", + "@language": "es" + }, + { + "@value": "Melone squash", + "@language": "it" + }, + { + "@value": "Squash melon", + "@language": "de" + }, + { + "@value": "Squash melon", + "@language": "sw" + }, + { + "@value": "Melancia de Squash", + "@language": "pt" + }, + { + "@value": "Squash melon", + "@language": "oc" + }, + { + "@value": "Сквош дыни", + "@language": "ru" + }, + { + "@value": "Squash melon", + "@language": "cy" + }, + { + "@value": "スカッシュメロン", + "@language": "ja" + }, + { + "@value": "Squash melon", + "@language": "ga" + }, + { + "@value": "तरबूज", + "@language": "hi" + }, + { + "@value": "Squash Ilon", + "@language": "zh" + }, + { + "@value": "Squash melon", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#squash", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#chervil", + "rdfs:label": [ + { + "@value": "Cerfeuil", + "@language": "fr" + }, + { + "@value": "Chervil", + "@language": "en" + }, + { + "@value": "شرن", + "@language": "ar" + }, + { + "@value": "Chervil", + "@language": "ku" + }, + { + "@value": "Chervil", + "@language": "es" + }, + { + "@value": "Chervil", + "@language": "it" + }, + { + "@value": "Kirschen", + "@language": "de" + }, + { + "@value": "Chervil", + "@language": "sw" + }, + { + "@value": "Cereja", + "@language": "pt" + }, + { + "@value": "Chervil", + "@language": "oc" + }, + { + "@value": "Червил", + "@language": "ru" + }, + { + "@value": "Chervil", + "@language": "cy" + }, + { + "@value": "チェロビル", + "@language": "ja" + }, + { + "@value": "riachtanais uisce: measartha", + "@language": "ga" + }, + { + "@value": "चेरविल", + "@language": "hi" + }, + { + "@value": "Chervil", + "@language": "zh" + }, + { + "@value": "Chervil", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#aromatic", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#savory-groceries", + "rdfs:label": [ + { + "@value": "Epicerie salée", + "@language": "fr" + }, + { + "@value": "Savory groceries", + "@language": "en" + }, + { + "@value": "البقالة الفاخرة", + "@language": "ar" + }, + { + "@value": "Savory groceries", + "@language": "ku" + }, + { + "@value": "Tiendas de alimentos", + "@language": "es" + }, + { + "@value": "Ciliegia salata", + "@language": "it" + }, + { + "@value": "Gemüse", + "@language": "de" + }, + { + "@value": "Savory groceries", + "@language": "sw" + }, + { + "@value": "Receitas de sabor", + "@language": "pt" + }, + { + "@value": "Savory groceries", + "@language": "oc" + }, + { + "@value": "Саварии продуктов", + "@language": "ru" + }, + { + "@value": "Savory groceries", + "@language": "cy" + }, + { + "@value": "サボリー食料品", + "@language": "ja" + }, + { + "@value": "Grúplanna blasta", + "@language": "ga" + }, + { + "@value": "Savory groceries", + "@language": "hi" + }, + { + "@value": "Savory 海洋学", + "@language": "zh" + }, + { + "@value": "Savory groceries", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#local-grocery-store", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#local-grocery-store", + "rdfs:label": [ + { + "@value": "Epicerie locale", + "@language": "fr" + }, + { + "@value": "Local grocery store", + "@language": "en" + }, + { + "@value": "محل البقالة المحلي", + "@language": "ar" + }, + { + "@value": "Local grocery store", + "@language": "ku" + }, + { + "@value": "Tienda local de comestibles", + "@language": "es" + }, + { + "@value": "Negozio di alimentari locale", + "@language": "it" + }, + { + "@value": "Lokale Lebensmittelgeschäft", + "@language": "de" + }, + { + "@value": "Local grocery store", + "@language": "sw" + }, + { + "@value": "Mercadorias locais", + "@language": "pt" + }, + { + "@value": "Local grocery store", + "@language": "oc" + }, + { + "@value": "Местный продуктовый магазин", + "@language": "ru" + }, + { + "@value": "Local grocery store", + "@language": "cy" + }, + { + "@value": "ローカル食料品店", + "@language": "ja" + }, + { + "@value": "Siopa grósaera áitiúil", + "@language": "ga" + }, + { + "@value": "स्थानीय किराने की दुकान", + "@language": "hi" + }, + { + "@value": "地方牧场", + "@language": "zh" + }, + { + "@value": "Local grocery store", + "@language": "ca" + } + ], + "dfc-p:specialize": "undefined", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#processed-fruit", + "rdfs:label": [ + { + "@value": "Fruit transformé", + "@language": "fr" + }, + { + "@value": "Processed fruit", + "@language": "en" + }, + { + "@value": "الفاكهة المعالجة", + "@language": "ar" + }, + { + "@value": "Processed fruit", + "@language": "ku" + }, + { + "@value": "Fruta procesada", + "@language": "es" + }, + { + "@value": "Frutta trasformata", + "@language": "it" + }, + { + "@value": "verarbeitete Früchte", + "@language": "de" + }, + { + "@value": "Processed fruit", + "@language": "sw" + }, + { + "@value": "Fruta processada", + "@language": "pt" + }, + { + "@value": "Processed fruit", + "@language": "oc" + }, + { + "@value": "Обрабатываемые фрукты", + "@language": "ru" + }, + { + "@value": "Processed fruit", + "@language": "cy" + }, + { + "@value": "加工果実", + "@language": "ja" + }, + { + "@value": "Torthaí a phróiseáil", + "@language": "ga" + }, + { + "@value": "संसाधित फल", + "@language": "hi" + }, + { + "@value": "进程成果", + "@language": "zh" + }, + { + "@value": "Processed fruit", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#local-grocery-store", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#other-dairy-product", + "rdfs:label": [ + { + "@value": "Produit laitier autre", + "@language": "fr" + }, + { + "@value": "Other Dairy product", + "@language": "en" + }, + { + "@value": "منتجات الألبان الأخرى", + "@language": "ar" + }, + { + "@value": "Other Dairy product", + "@language": "ku" + }, + { + "@value": "Otros productos lácteos", + "@language": "es" + }, + { + "@value": "Altri prodotti lattiero-caseari", + "@language": "it" + }, + { + "@value": "Andere Milchprodukte", + "@language": "de" + }, + { + "@value": "Other Dairy product", + "@language": "sw" + }, + { + "@value": "Outros produtos Dairy", + "@language": "pt" + }, + { + "@value": "Other Dairy product", + "@language": "oc" + }, + { + "@value": "Другой молочный продукт", + "@language": "ru" + }, + { + "@value": "Other Dairy product", + "@language": "cy" + }, + { + "@value": "その他の製品", + "@language": "ja" + }, + { + "@value": "Táirge déiríochta eile", + "@language": "ga" + }, + { + "@value": "अन्य डेयरी उत्पाद", + "@language": "hi" + }, + { + "@value": "其他空产品", + "@language": "zh" + }, + { + "@value": "Other Dairy product", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#potato", + "rdfs:label": [ + { + "@value": "Pomme de terre", + "@language": "fr" + }, + { + "@value": "Potato", + "@language": "en" + }, + { + "@value": "بطاطس", + "@language": "ar" + }, + { + "@value": "Potato", + "@language": "ku" + }, + { + "@value": "Potato", + "@language": "es" + }, + { + "@value": "Patate", + "@language": "it" + }, + { + "@value": "Kartoffeln", + "@language": "de" + }, + { + "@value": "Potato", + "@language": "sw" + }, + { + "@value": "Potato", + "@language": "pt" + }, + { + "@value": "Potato", + "@language": "oc" + }, + { + "@value": "Картофель", + "@language": "ru" + }, + { + "@value": "Potato", + "@language": "cy" + }, + { + "@value": "ポテト", + "@language": "ja" + }, + { + "@value": "Prátaí", + "@language": "ga" + }, + { + "@value": "आलू", + "@language": "hi" + }, + { + "@value": "波塔托", + "@language": "zh" + }, + { + "@value": "Potato", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#coriander", + "rdfs:label": [ + { + "@value": "Coriandre", + "@language": "fr" + }, + { + "@value": "Coriander", + "@language": "en" + }, + { + "@value": "Coriander", + "@language": "ar" + }, + { + "@value": "Coriander", + "@language": "ku" + }, + { + "@value": "Coriander", + "@language": "es" + }, + { + "@value": "Corigliera", + "@language": "it" + }, + { + "@value": "Koriander", + "@language": "de" + }, + { + "@value": "Coriander", + "@language": "sw" + }, + { + "@value": "Coriandro", + "@language": "pt" + }, + { + "@value": "Coriander", + "@language": "oc" + }, + { + "@value": "Кориандер", + "@language": "ru" + }, + { + "@value": "Coriander", + "@language": "cy" + }, + { + "@value": "コリアンダー", + "@language": "ja" + }, + { + "@value": "Coriander", + "@language": "ga" + }, + { + "@value": "धनिया", + "@language": "hi" + }, + { + "@value": "Coriander", + "@language": "zh" + }, + { + "@value": "Coriander", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#aromatic", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#beans", + "rdfs:label": [ + { + "@value": "Fèves", + "@language": "fr" + }, + { + "@value": "Beans", + "@language": "en" + }, + { + "@value": "الفاصوليا", + "@language": "ar" + }, + { + "@value": "Beans", + "@language": "ku" + }, + { + "@value": "Beans", + "@language": "es" + }, + { + "@value": "Fagioli", + "@language": "it" + }, + { + "@value": "Bohnen", + "@language": "de" + }, + { + "@value": "Beans", + "@language": "sw" + }, + { + "@value": "Feijões", + "@language": "pt" + }, + { + "@value": "Beans", + "@language": "oc" + }, + { + "@value": "Бобы", + "@language": "ru" + }, + { + "@value": "Beans", + "@language": "cy" + }, + { + "@value": "ビーンズ", + "@language": "ja" + }, + { + "@value": "Pónairí", + "@language": "ga" + }, + { + "@value": "बीन", + "@language": "hi" + }, + { + "@value": "贝 尔", + "@language": "zh" + }, + { + "@value": "Beans", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#dried-vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#viennoiserie-", + "rdfs:label": [ + { + "@value": "Viennoiserie", + "@language": "fr" + }, + { + "@value": "Viennoiserie ", + "@language": "en" + }, + { + "@value": "Viennoiserie", + "@language": "ar" + }, + { + "@value": "Viennoiserie ", + "@language": "ku" + }, + { + "@value": "Viennoiserie", + "@language": "es" + }, + { + "@value": "Viennoiserie", + "@language": "it" + }, + { + "@value": "Das ist eine gute Idee.", + "@language": "de" + }, + { + "@value": "Viennoiserie ", + "@language": "sw" + }, + { + "@value": "Viennoise", + "@language": "pt" + }, + { + "@value": "Viennoiserie ", + "@language": "oc" + }, + { + "@value": "Вена", + "@language": "ru" + }, + { + "@value": "Viennoiserie ", + "@language": "cy" + }, + { + "@value": "ヴィエンノワリエ", + "@language": "ja" + }, + { + "@value": "bláthanna cumhra: cumhráin", + "@language": "ga" + }, + { + "@value": "विनोसेरी", + "@language": "hi" + }, + { + "@value": "Viennorie", + "@language": "zh" + }, + { + "@value": "Viennoiserie ", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#bakery", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#goat-flavored-yogurt", + "rdfs:label": [ + { + "@value": "Yaourt aromatisé", + "@language": "fr" + }, + { + "@value": "Goat flavored yogurt", + "@language": "en" + }, + { + "@value": "زبادة نكهة قوطية", + "@language": "ar" + }, + { + "@value": "Goat flavored yogurt", + "@language": "ku" + }, + { + "@value": "Yogurt con sabor a cabra", + "@language": "es" + }, + { + "@value": "yogurt aromatizzato alla gola", + "@language": "it" + }, + { + "@value": "Ziegen aromatisierter Joghurt", + "@language": "de" + }, + { + "@value": "Goat flavored yogurt", + "@language": "sw" + }, + { + "@value": "Iogurte saboroso de cabra", + "@language": "pt" + }, + { + "@value": "Goat flavored yogurt", + "@language": "oc" + }, + { + "@value": "Говяной ароматный йогурт", + "@language": "ru" + }, + { + "@value": "Goat flavored yogurt", + "@language": "cy" + }, + { + "@value": "ヤギ風味ヨーグルト", + "@language": "ja" + }, + { + "@value": "iógart blas Goat", + "@language": "ga" + }, + { + "@value": "बकरी स्वादयुक्त दही", + "@language": "hi" + }, + { + "@value": "Gatrif yogurt", + "@language": "zh" + }, + { + "@value": "Goat flavored yogurt", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#goat-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#currant", + "rdfs:label": [ + { + "@value": "Groseille", + "@language": "fr" + }, + { + "@value": "Currant", + "@language": "en" + }, + { + "@value": "كوران", + "@language": "ar" + }, + { + "@value": "Currant", + "@language": "ku" + }, + { + "@value": "Currant", + "@language": "es" + }, + { + "@value": "Maledizione", + "@language": "it" + }, + { + "@value": "Johannisbeeren", + "@language": "de" + }, + { + "@value": "Currant", + "@language": "sw" + }, + { + "@value": "Currant", + "@language": "pt" + }, + { + "@value": "Currant", + "@language": "oc" + }, + { + "@value": "Смородина", + "@language": "ru" + }, + { + "@value": "Currant", + "@language": "cy" + }, + { + "@value": "ログイン", + "@language": "ja" + }, + { + "@value": "Currant", + "@language": "ga" + }, + { + "@value": "गारंटी", + "@language": "hi" + }, + { + "@value": "扣押", + "@language": "zh" + }, + { + "@value": "Currant", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#berry", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#nectarine", + "rdfs:label": [ + { + "@value": "Nectarine", + "@language": "fr" + }, + { + "@value": "Nectarine", + "@language": "en" + }, + { + "@value": "Nectarine", + "@language": "ar" + }, + { + "@value": "Nectarine", + "@language": "ku" + }, + { + "@value": "Nectarina", + "@language": "es" + }, + { + "@value": "Nettatura", + "@language": "it" + }, + { + "@value": "Nektarin", + "@language": "de" + }, + { + "@value": "Nectarine", + "@language": "sw" + }, + { + "@value": "Nectarina", + "@language": "pt" + }, + { + "@value": "Nectarine", + "@language": "oc" + }, + { + "@value": "Нектарин", + "@language": "ru" + }, + { + "@value": "Nectarine", + "@language": "cy" + }, + { + "@value": "ネクタール", + "@language": "ja" + }, + { + "@value": "riachtanais uisce: measartha", + "@language": "ga" + }, + { + "@value": "नेक्टारिन", + "@language": "hi" + }, + { + "@value": "注", + "@language": "zh" + }, + { + "@value": "Nectarine", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#fruit", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#bread", + "rdfs:label": [ + { + "@value": "Pain", + "@language": "fr" + }, + { + "@value": "Bread", + "@language": "en" + }, + { + "@value": "الخبز", + "@language": "ar" + }, + { + "@value": "Bread", + "@language": "ku" + }, + { + "@value": "Pan", + "@language": "es" + }, + { + "@value": "Pane", + "@language": "it" + }, + { + "@value": "Brot", + "@language": "de" + }, + { + "@value": "Bread", + "@language": "sw" + }, + { + "@value": "Pão", + "@language": "pt" + }, + { + "@value": "Bread", + "@language": "oc" + }, + { + "@value": "Хлеб", + "@language": "ru" + }, + { + "@value": "Bread", + "@language": "cy" + }, + { + "@value": "フリガナ", + "@language": "ja" + }, + { + "@value": "Arán", + "@language": "ga" + }, + { + "@value": "ब्रेड", + "@language": "hi" + }, + { + "@value": "内容提要", + "@language": "zh" + }, + { + "@value": "Bread", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#bakery", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#sheep-sweet-yogurt", + "rdfs:label": [ + { + "@value": "Yaourt sucré", + "@language": "fr" + }, + { + "@value": "Sheep sweet yogurt", + "@language": "en" + }, + { + "@value": "الزبادي الحلو", + "@language": "ar" + }, + { + "@value": "Sheep sweet yogurt", + "@language": "ku" + }, + { + "@value": "Oveja dulce yogurt", + "@language": "es" + }, + { + "@value": "yogurt dolce di pecore", + "@language": "it" + }, + { + "@value": "Schaf süßer Joghurt", + "@language": "de" + }, + { + "@value": "Sheep sweet yogurt", + "@language": "sw" + }, + { + "@value": "Iogurte doce de ovelhas", + "@language": "pt" + }, + { + "@value": "Sheep sweet yogurt", + "@language": "oc" + }, + { + "@value": "Овцы сладкий йогурт", + "@language": "ru" + }, + { + "@value": "Sheep sweet yogurt", + "@language": "cy" + }, + { + "@value": "羊の甘いヨーグルト", + "@language": "ja" + }, + { + "@value": "Iógart milis caorach", + "@language": "ga" + }, + { + "@value": "भेड़ मीठा दही", + "@language": "hi" + }, + { + "@value": "她在家中偷窃", + "@language": "zh" + }, + { + "@value": "Sheep sweet yogurt", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#sheep-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#smooth-cabbage", + "rdfs:label": [ + { + "@value": "Chou lisse", + "@language": "fr" + }, + { + "@value": "Smooth cabbage", + "@language": "en" + }, + { + "@value": "كبش فاسد", + "@language": "ar" + }, + { + "@value": "Smooth cabbage", + "@language": "ku" + }, + { + "@value": "Smooth cabbage", + "@language": "es" + }, + { + "@value": "Cavolo liscio", + "@language": "it" + }, + { + "@value": "Glatte Kohl", + "@language": "de" + }, + { + "@value": "Smooth cabbage", + "@language": "sw" + }, + { + "@value": "Repolho suave", + "@language": "pt" + }, + { + "@value": "Smooth cabbage", + "@language": "oc" + }, + { + "@value": "Гладкая капуста", + "@language": "ru" + }, + { + "@value": "Smooth cabbage", + "@language": "cy" + }, + { + "@value": "滑らかなキャベツ", + "@language": "ja" + }, + { + "@value": "Cabáiste réidh", + "@language": "ga" + }, + { + "@value": "चिकना गोभी", + "@language": "hi" + }, + { + "@value": "Smooth cabbage", + "@language": "zh" + }, + { + "@value": "Smooth cabbage", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#cabbage", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#courgette", + "rdfs:label": [ + { + "@value": "Courgette", + "@language": "fr" + }, + { + "@value": "Courgette", + "@language": "en" + }, + { + "@value": "Courgette", + "@language": "ar" + }, + { + "@value": "Courgette", + "@language": "ku" + }, + { + "@value": "Courgette", + "@language": "es" + }, + { + "@value": "Courgette", + "@language": "it" + }, + { + "@value": "Mut", + "@language": "de" + }, + { + "@value": "Courgette", + "@language": "sw" + }, + { + "@value": "Obrigadinho.", + "@language": "pt" + }, + { + "@value": "Courgette", + "@language": "oc" + }, + { + "@value": "Кургет", + "@language": "ru" + }, + { + "@value": "Courgette", + "@language": "cy" + }, + { + "@value": "クーデッテ", + "@language": "ja" + }, + { + "@value": "Courgette", + "@language": "ga" + }, + { + "@value": "Courgette", + "@language": "hi" + }, + { + "@value": "联合特色", + "@language": "zh" + }, + { + "@value": "Courgette", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#strawberry", + "rdfs:label": [ + { + "@value": "Fraise", + "@language": "fr" + }, + { + "@value": "Strawberry", + "@language": "en" + }, + { + "@value": "الفراولة", + "@language": "ar" + }, + { + "@value": "Strawberry", + "@language": "ku" + }, + { + "@value": "Fresa", + "@language": "es" + }, + { + "@value": "Fragola", + "@language": "it" + }, + { + "@value": "Erdbeeren", + "@language": "de" + }, + { + "@value": "Strawberry", + "@language": "sw" + }, + { + "@value": "Morango", + "@language": "pt" + }, + { + "@value": "Strawberry", + "@language": "oc" + }, + { + "@value": "Клубника", + "@language": "ru" + }, + { + "@value": "Strawberry", + "@language": "cy" + }, + { + "@value": "ストロベリー", + "@language": "ja" + }, + { + "@value": "bláthanna cumhra: cumhráin", + "@language": "ga" + }, + { + "@value": "स्ट्रॉबेरी", + "@language": "hi" + }, + { + "@value": "原状", + "@language": "zh" + }, + { + "@value": "Strawberry", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#fruit", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#goat-natural-yogurt", + "rdfs:label": [ + { + "@value": "Yaourt nature", + "@language": "fr" + }, + { + "@value": "Goat natural yogurt", + "@language": "en" + }, + { + "@value": "Goat natural yogurt", + "@language": "ar" + }, + { + "@value": "Goat natural yogurt", + "@language": "ku" + }, + { + "@value": "Yogur natural", + "@language": "es" + }, + { + "@value": "Goat yogurt naturale", + "@language": "it" + }, + { + "@value": "Ziegen natürlicher Joghurt", + "@language": "de" + }, + { + "@value": "Goat natural yogurt", + "@language": "sw" + }, + { + "@value": "Iogurte natural de cabra", + "@language": "pt" + }, + { + "@value": "Goat natural yogurt", + "@language": "oc" + }, + { + "@value": "Говядина натуральная йогурт", + "@language": "ru" + }, + { + "@value": "Goat natural yogurt", + "@language": "cy" + }, + { + "@value": "ヤギの天然ヨーグルト", + "@language": "ja" + }, + { + "@value": "iógart nádúrtha Goat", + "@language": "ga" + }, + { + "@value": "बकरी प्राकृतिक दही", + "@language": "hi" + }, + { + "@value": "Gat自然 yogurt", + "@language": "zh" + }, + { + "@value": "Goat natural yogurt", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#goat-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#pastry", + "rdfs:label": [ + { + "@value": "Pâtisserie", + "@language": "fr" + }, + { + "@value": "Pastry", + "@language": "en" + }, + { + "@value": "براس", + "@language": "ar" + }, + { + "@value": "Pastry", + "@language": "ku" + }, + { + "@value": "Pastelería", + "@language": "es" + }, + { + "@value": "Pasticceria", + "@language": "it" + }, + { + "@value": "Backwaren", + "@language": "de" + }, + { + "@value": "Pastry", + "@language": "sw" + }, + { + "@value": "Pasta", + "@language": "pt" + }, + { + "@value": "Pastry", + "@language": "oc" + }, + { + "@value": "Кондитерская", + "@language": "ru" + }, + { + "@value": "Pastry", + "@language": "cy" + }, + { + "@value": "ペストリー", + "@language": "ja" + }, + { + "@value": "An tIarthar", + "@language": "ga" + }, + { + "@value": "पेस्ट्री", + "@language": "hi" + }, + { + "@value": "工业", + "@language": "zh" + }, + { + "@value": "Pastry", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#sweet-groceries", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#fresh-cheese", + "rdfs:label": [ + { + "@value": "Fromage frais", + "@language": "fr" + }, + { + "@value": "Fresh cheese", + "@language": "en" + }, + { + "@value": "جبنة طازجة", + "@language": "ar" + }, + { + "@value": "Fresh cheese", + "@language": "ku" + }, + { + "@value": "Queso fresco", + "@language": "es" + }, + { + "@value": "Formaggi freschi", + "@language": "it" + }, + { + "@value": "Frischkäse", + "@language": "de" + }, + { + "@value": "Fresh cheese", + "@language": "sw" + }, + { + "@value": "Queijo fresco", + "@language": "pt" + }, + { + "@value": "Fresh cheese", + "@language": "oc" + }, + { + "@value": "Свежий сыр", + "@language": "ru" + }, + { + "@value": "Fresh cheese", + "@language": "cy" + }, + { + "@value": "フレッシュチーズ", + "@language": "ja" + }, + { + "@value": "Cáis úr", + "@language": "ga" + }, + { + "@value": "ताजा पनीर", + "@language": "hi" + }, + { + "@value": "淡水哺乳动物", + "@language": "zh" + }, + { + "@value": "Fresh cheese", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#cow-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#canned-vegetable", + "rdfs:label": [ + { + "@value": "Légume en conserve", + "@language": "fr" + }, + { + "@value": "Canned vegetable", + "@language": "en" + }, + { + "@value": "الخضروات المعلبة", + "@language": "ar" + }, + { + "@value": "Canned vegetable", + "@language": "ku" + }, + { + "@value": "Verdura enlatada", + "@language": "es" + }, + { + "@value": "Ortaggi in scatola", + "@language": "it" + }, + { + "@value": "Gemüse", + "@language": "de" + }, + { + "@value": "Canned vegetable", + "@language": "sw" + }, + { + "@value": "Vegetal em conserva", + "@language": "pt" + }, + { + "@value": "Canned vegetable", + "@language": "oc" + }, + { + "@value": "Консервированные овощи", + "@language": "ru" + }, + { + "@value": "Canned vegetable", + "@language": "cy" + }, + { + "@value": "缶詰野菜", + "@language": "ja" + }, + { + "@value": "Glasraí stánaithe", + "@language": "ga" + }, + { + "@value": "डिब्बाबंद सब्जी", + "@language": "hi" + }, + { + "@value": "种植蔬菜", + "@language": "zh" + }, + { + "@value": "Canned vegetable", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#processed-vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#yogurt-on-a-bed-of-fruit", + "rdfs:label": [ + { + "@value": "Yaourt sur lit de fruit", + "@language": "fr" + }, + { + "@value": "Yogurt on a bed of fruit", + "@language": "en" + }, + { + "@value": "يوغاد على سرير الفاكهة", + "@language": "ar" + }, + { + "@value": "Yogurt on a bed of fruit", + "@language": "ku" + }, + { + "@value": "Yogur en una cama de fruta", + "@language": "es" + }, + { + "@value": "Yogurt su un letto di frutta", + "@language": "it" + }, + { + "@value": "Joghurt auf einem Bett mit Obst", + "@language": "de" + }, + { + "@value": "Yogurt on a bed of fruit", + "@language": "sw" + }, + { + "@value": "iogurte em uma cama de frutas", + "@language": "pt" + }, + { + "@value": "Yogurt on a bed of fruit", + "@language": "oc" + }, + { + "@value": "Йогурт на кровати фруктов", + "@language": "ru" + }, + { + "@value": "Yogurt on a bed of fruit", + "@language": "cy" + }, + { + "@value": "フルーツのベッドにヨーグルト", + "@language": "ja" + }, + { + "@value": "Yogurt ar leaba torthaí", + "@language": "ga" + }, + { + "@value": "एक बिस्तर पर दही", + "@language": "hi" + }, + { + "@value": "一. 果敢", + "@language": "zh" + }, + { + "@value": "Yogurt on a bed of fruit", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#cow-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#aperitif", + "rdfs:label": [ + { + "@value": "Apéritif", + "@language": "fr" + }, + { + "@value": "Aperitif", + "@language": "en" + }, + { + "@value": "Aperitif", + "@language": "ar" + }, + { + "@value": "Aperitif", + "@language": "ku" + }, + { + "@value": "Aperitivo", + "@language": "es" + }, + { + "@value": "Aperitivo", + "@language": "it" + }, + { + "@value": "Aperitif", + "@language": "de" + }, + { + "@value": "Aperitif", + "@language": "sw" + }, + { + "@value": "Aperitivo", + "@language": "pt" + }, + { + "@value": "Aperitif", + "@language": "oc" + }, + { + "@value": "Аперитив", + "@language": "ru" + }, + { + "@value": "Aperitif", + "@language": "cy" + }, + { + "@value": "アペリティフ", + "@language": "ja" + }, + { + "@value": "An t-eolas úsáideach", + "@language": "ga" + }, + { + "@value": "Aperitif", + "@language": "hi" + }, + { + "@value": "A. 启动", + "@language": "zh" + }, + { + "@value": "Aperitif", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#alcoholic-beverage", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#kale", + "rdfs:label": [ + { + "@value": "Chou frisé", + "@language": "fr" + }, + { + "@value": "Kale", + "@language": "en" + }, + { + "@value": "كالي", + "@language": "ar" + }, + { + "@value": "Kale", + "@language": "ku" + }, + { + "@value": "Kale", + "@language": "es" + }, + { + "@value": "Kale", + "@language": "it" + }, + { + "@value": "Kalbs", + "@language": "de" + }, + { + "@value": "Kale", + "@language": "sw" + }, + { + "@value": "Kale", + "@language": "pt" + }, + { + "@value": "Kale", + "@language": "oc" + }, + { + "@value": "Кале", + "@language": "ru" + }, + { + "@value": "Kale", + "@language": "cy" + }, + { + "@value": "ケール", + "@language": "ja" + }, + { + "@value": "taiseachas aeir: fliuch", + "@language": "ga" + }, + { + "@value": "कोल", + "@language": "hi" + }, + { + "@value": "Kale", + "@language": "zh" + }, + { + "@value": "Kale", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#cabbage", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#grilling-meat", + "rdfs:label": [ + { + "@value": "Viande à griller", + "@language": "fr" + }, + { + "@value": "Grilling meat", + "@language": "en" + }, + { + "@value": "لحم مُحن", + "@language": "ar" + }, + { + "@value": "Grilling meat", + "@language": "ku" + }, + { + "@value": "Carne a la plancha", + "@language": "es" + }, + { + "@value": "Carne alla griglia", + "@language": "it" + }, + { + "@value": "Grillfleisch", + "@language": "de" + }, + { + "@value": "Grilling meat", + "@language": "sw" + }, + { + "@value": "Grelhados", + "@language": "pt" + }, + { + "@value": "Grilling meat", + "@language": "oc" + }, + { + "@value": "Гриль мясо", + "@language": "ru" + }, + { + "@value": "Grilling meat", + "@language": "cy" + }, + { + "@value": "肉のグリル", + "@language": "ja" + }, + { + "@value": "Meilt feola", + "@language": "ga" + }, + { + "@value": "grilling मांस", + "@language": "hi" + }, + { + "@value": "G. 导 言", + "@language": "zh" + }, + { + "@value": "Grilling meat", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#beef", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#processed-vegetable", + "rdfs:label": [ + { + "@value": "Légume transformé", + "@language": "fr" + }, + { + "@value": "Processed vegetable", + "@language": "en" + }, + { + "@value": "الخضروات المجهزة", + "@language": "ar" + }, + { + "@value": "Processed vegetable", + "@language": "ku" + }, + { + "@value": "Verdura procesada", + "@language": "es" + }, + { + "@value": "Ortaggi lavorati", + "@language": "it" + }, + { + "@value": "Verarbeitetes Gemüse", + "@language": "de" + }, + { + "@value": "Processed vegetable", + "@language": "sw" + }, + { + "@value": "Vegetação processada", + "@language": "pt" + }, + { + "@value": "Processed vegetable", + "@language": "oc" + }, + { + "@value": "Обработанный овощ", + "@language": "ru" + }, + { + "@value": "Processed vegetable", + "@language": "cy" + }, + { + "@value": "加工野菜", + "@language": "ja" + }, + { + "@value": "Glasraí próiseáilte", + "@language": "ga" + }, + { + "@value": "संसाधित सब्जी", + "@language": "hi" + }, + { + "@value": "加工蔬菜", + "@language": "zh" + }, + { + "@value": "Processed vegetable", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#local-grocery-store", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#spinach", + "rdfs:label": [ + { + "@value": "Épinard", + "@language": "fr" + }, + { + "@value": "Spinach", + "@language": "en" + }, + { + "@value": "Spinach", + "@language": "ar" + }, + { + "@value": "Spinach", + "@language": "ku" + }, + { + "@value": "Spinach", + "@language": "es" + }, + { + "@value": "Spinaci", + "@language": "it" + }, + { + "@value": "Spinat", + "@language": "de" + }, + { + "@value": "Spinach", + "@language": "sw" + }, + { + "@value": "Espinafre", + "@language": "pt" + }, + { + "@value": "Spinach", + "@language": "oc" + }, + { + "@value": "Спинат", + "@language": "ru" + }, + { + "@value": "Spinach", + "@language": "cy" + }, + { + "@value": "スピナッチ", + "@language": "ja" + }, + { + "@value": "riachtanais uisce: measartha", + "@language": "ga" + }, + { + "@value": "पालक", + "@language": "hi" + }, + { + "@value": "每个支柱", + "@language": "zh" + }, + { + "@value": "Spinach", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#salad", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#morel", + "rdfs:label": [ + { + "@value": "Morille", + "@language": "fr" + }, + { + "@value": "Morel", + "@language": "en" + }, + { + "@value": "Morel", + "@language": "ar" + }, + { + "@value": "Morel", + "@language": "ku" + }, + { + "@value": "Morel", + "@language": "es" + }, + { + "@value": "Altri", + "@language": "it" + }, + { + "@value": "Mehr", + "@language": "de" + }, + { + "@value": "Morel", + "@language": "sw" + }, + { + "@value": "Morel", + "@language": "pt" + }, + { + "@value": "Morel", + "@language": "oc" + }, + { + "@value": "Большел", + "@language": "ru" + }, + { + "@value": "Morel", + "@language": "cy" + }, + { + "@value": "もっと詳しく", + "@language": "ja" + }, + { + "@value": "Tuilleadh eolais", + "@language": "ga" + }, + { + "@value": "Morel", + "@language": "hi" + }, + { + "@value": "增加", + "@language": "zh" + }, + { + "@value": "Morel", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#mushroom", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#cucumber", + "rdfs:label": [ + { + "@value": "Concombre", + "@language": "fr" + }, + { + "@value": "Cucumber", + "@language": "en" + }, + { + "@value": "الخيار", + "@language": "ar" + }, + { + "@value": "Cucumber", + "@language": "ku" + }, + { + "@value": "Cucumber", + "@language": "es" + }, + { + "@value": "Cetriolo", + "@language": "it" + }, + { + "@value": "Gurken", + "@language": "de" + }, + { + "@value": "Cucumber", + "@language": "sw" + }, + { + "@value": "Pepino", + "@language": "pt" + }, + { + "@value": "Cucumber", + "@language": "oc" + }, + { + "@value": "огурец", + "@language": "ru" + }, + { + "@value": "Cucumber", + "@language": "cy" + }, + { + "@value": "キュウリ", + "@language": "ja" + }, + { + "@value": "Cuan", + "@language": "ga" + }, + { + "@value": "ककड़ी", + "@language": "hi" + }, + { + "@value": "现任法官", + "@language": "zh" + }, + { + "@value": "Cucumber", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#onion", + "rdfs:label": [ + { + "@value": "Oignon", + "@language": "fr" + }, + { + "@value": "Onion", + "@language": "en" + }, + { + "@value": "Onion", + "@language": "ar" + }, + { + "@value": "Onion", + "@language": "ku" + }, + { + "@value": "cebolla", + "@language": "es" + }, + { + "@value": "Cipolla", + "@language": "it" + }, + { + "@value": "Zwiebel", + "@language": "de" + }, + { + "@value": "Onion", + "@language": "sw" + }, + { + "@value": "A cebola", + "@language": "pt" + }, + { + "@value": "Onion", + "@language": "oc" + }, + { + "@value": "Лук", + "@language": "ru" + }, + { + "@value": "Onion", + "@language": "cy" + }, + { + "@value": "オニオン", + "@language": "ja" + }, + { + "@value": "An tIomlán", + "@language": "ga" + }, + { + "@value": "प्याज", + "@language": "hi" + }, + { + "@value": "弃 权", + "@language": "zh" + }, + { + "@value": "Onion", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#artichoke", + "rdfs:label": [ + { + "@value": "Artichaut", + "@language": "fr" + }, + { + "@value": "Artichoke", + "@language": "en" + }, + { + "@value": "Artichoke", + "@language": "ar" + }, + { + "@value": "Artichoke", + "@language": "ku" + }, + { + "@value": "Artichoke", + "@language": "es" + }, + { + "@value": "Carciofo", + "@language": "it" + }, + { + "@value": "Artischocke", + "@language": "de" + }, + { + "@value": "Artichoke", + "@language": "sw" + }, + { + "@value": "Anúncio", + "@language": "pt" + }, + { + "@value": "Artichoke", + "@language": "oc" + }, + { + "@value": "Артишок", + "@language": "ru" + }, + { + "@value": "Artichoke", + "@language": "cy" + }, + { + "@value": "アーティチョーク", + "@language": "ja" + }, + { + "@value": "Déan teagmháil linn", + "@language": "ga" + }, + { + "@value": "आर्टिचोक", + "@language": "hi" + }, + { + "@value": "阿尔奇", + "@language": "zh" + }, + { + "@value": "Artichoke", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#goat-milk", + "rdfs:label": [ + { + "@value": "Lait", + "@language": "fr" + }, + { + "@value": "Goat Milk", + "@language": "en" + }, + { + "@value": "Goat Milk", + "@language": "ar" + }, + { + "@value": "Goat Milk", + "@language": "ku" + }, + { + "@value": "Goat Milk", + "@language": "es" + }, + { + "@value": "Latte di capra", + "@language": "it" + }, + { + "@value": "Ziegenmilch", + "@language": "de" + }, + { + "@value": "Goat Milk", + "@language": "sw" + }, + { + "@value": "Leite de cabra", + "@language": "pt" + }, + { + "@value": "Goat Milk", + "@language": "oc" + }, + { + "@value": "Говяное молоко", + "@language": "ru" + }, + { + "@value": "Goat Milk", + "@language": "cy" + }, + { + "@value": "ヤギミルク", + "@language": "ja" + }, + { + "@value": "Bainne Goat", + "@language": "ga" + }, + { + "@value": "बकरी दूध", + "@language": "hi" + }, + { + "@value": "Gat Milk", + "@language": "zh" + }, + { + "@value": "Goat Milk", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#goat-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#nut", + "rdfs:label": [ + { + "@value": "Fruit à coque", + "@language": "fr" + }, + { + "@value": "Nut", + "@language": "en" + }, + { + "@value": "Nut", + "@language": "ar" + }, + { + "@value": "Nut", + "@language": "ku" + }, + { + "@value": "Nut", + "@language": "es" + }, + { + "@value": "Nut", + "@language": "it" + }, + { + "@value": "Mutter", + "@language": "de" + }, + { + "@value": "Nut", + "@language": "sw" + }, + { + "@value": "Nut", + "@language": "pt" + }, + { + "@value": "Nut", + "@language": "oc" + }, + { + "@value": "Ню", + "@language": "ru" + }, + { + "@value": "Nut", + "@language": "cy" + }, + { + "@value": "ナッツ", + "@language": "ja" + }, + { + "@value": "Cnó", + "@language": "ga" + }, + { + "@value": "अखरोट", + "@language": "hi" + }, + { + "@value": "Nut", + "@language": "zh" + }, + { + "@value": "Nut", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#fruit", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#dairy-product", + "rdfs:label": [ + { + "@value": "Produit laitier", + "@language": "fr" + }, + { + "@value": "Dairy product", + "@language": "en" + }, + { + "@value": "منتجات الألبان", + "@language": "ar" + }, + { + "@value": "Dairy product", + "@language": "ku" + }, + { + "@value": "Productos lácteos", + "@language": "es" + }, + { + "@value": "Prodotto lattiero-caseario", + "@language": "it" + }, + { + "@value": "Milchprodukte", + "@language": "de" + }, + { + "@value": "Dairy product", + "@language": "sw" + }, + { + "@value": "Produtos lácteos", + "@language": "pt" + }, + { + "@value": "Dairy product", + "@language": "oc" + }, + { + "@value": "Молочный продукт", + "@language": "ru" + }, + { + "@value": "Dairy product", + "@language": "cy" + }, + { + "@value": "乳製品製品製品", + "@language": "ja" + }, + { + "@value": "táirge déiríochta", + "@language": "ga" + }, + { + "@value": "डेयरी उत्पाद", + "@language": "hi" + }, + { + "@value": "Dair产品", + "@language": "zh" + }, + { + "@value": "Dairy product", + "@language": "ca" + } + ], + "dfc-p:specialize": "undefined", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#red-kuri-squash", + "rdfs:label": [ + { + "@value": "Potimarron", + "@language": "fr" + }, + { + "@value": "Red kuri squash", + "@language": "en" + }, + { + "@value": "سكواش الكاري الأحمر", + "@language": "ar" + }, + { + "@value": "Red kuri squash", + "@language": "ku" + }, + { + "@value": "Escuadrón rojo kuri", + "@language": "es" + }, + { + "@value": "Rosso kuri squash", + "@language": "it" + }, + { + "@value": "Rote Eiche", + "@language": "de" + }, + { + "@value": "Red kuri squash", + "@language": "sw" + }, + { + "@value": "Vermelho kuri squash", + "@language": "pt" + }, + { + "@value": "Red kuri squash", + "@language": "oc" + }, + { + "@value": "Красная куриная сквоша", + "@language": "ru" + }, + { + "@value": "Red kuri squash", + "@language": "cy" + }, + { + "@value": "レッドクリスカッシュ", + "@language": "ja" + }, + { + "@value": "cliceáil grianghraf a mhéadú", + "@language": "ga" + }, + { + "@value": "लाल कुरी स्क्वैश", + "@language": "hi" + }, + { + "@value": "红十字会的Kuri车队", + "@language": "zh" + }, + { + "@value": "Red kuri squash", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#squash", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#squash", + "rdfs:label": [ + { + "@value": "Courge", + "@language": "fr" + }, + { + "@value": "Squash", + "@language": "en" + }, + { + "@value": "Squash", + "@language": "ar" + }, + { + "@value": "Squash", + "@language": "ku" + }, + { + "@value": "Squash", + "@language": "es" + }, + { + "@value": "Squash", + "@language": "it" + }, + { + "@value": "Squash", + "@language": "de" + }, + { + "@value": "Squash", + "@language": "sw" + }, + { + "@value": "Squash", + "@language": "pt" + }, + { + "@value": "Squash", + "@language": "oc" + }, + { + "@value": "Сквош", + "@language": "ru" + }, + { + "@value": "Squash", + "@language": "cy" + }, + { + "@value": "スカッシュ", + "@language": "ja" + }, + { + "@value": "Squash", + "@language": "ga" + }, + { + "@value": "स्क्वैश", + "@language": "hi" + }, + { + "@value": "绑架", + "@language": "zh" + }, + { + "@value": "Squash", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#frozen-vegetable", + "rdfs:label": [ + { + "@value": "Légume surgelé", + "@language": "fr" + }, + { + "@value": "Frozen vegetable", + "@language": "en" + }, + { + "@value": "خضروات مجمدة", + "@language": "ar" + }, + { + "@value": "Frozen vegetable", + "@language": "ku" + }, + { + "@value": "vegetales congelados", + "@language": "es" + }, + { + "@value": "Ortaggi congelati", + "@language": "it" + }, + { + "@value": "Gefrorenes Gemüse", + "@language": "de" + }, + { + "@value": "Frozen vegetable", + "@language": "sw" + }, + { + "@value": "Vegetação congelada", + "@language": "pt" + }, + { + "@value": "Frozen vegetable", + "@language": "oc" + }, + { + "@value": "Замороженный овощ", + "@language": "ru" + }, + { + "@value": "Frozen vegetable", + "@language": "cy" + }, + { + "@value": "冷凍野菜", + "@language": "ja" + }, + { + "@value": "Glasraí reoite", + "@language": "ga" + }, + { + "@value": "जमे हुए सब्जी", + "@language": "hi" + }, + { + "@value": "Frozen蔬菜", + "@language": "zh" + }, + { + "@value": "Frozen vegetable", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#frozen", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#sage", + "rdfs:label": [ + { + "@value": "Sauge", + "@language": "fr" + }, + { + "@value": "Sage", + "@language": "en" + }, + { + "@value": "Sage", + "@language": "ar" + }, + { + "@value": "Sage", + "@language": "ku" + }, + { + "@value": "Sage", + "@language": "es" + }, + { + "@value": "Saggio", + "@language": "it" + }, + { + "@value": "Salbei", + "@language": "de" + }, + { + "@value": "Sage", + "@language": "sw" + }, + { + "@value": "Sage", + "@language": "pt" + }, + { + "@value": "Sage", + "@language": "oc" + }, + { + "@value": "Клетка", + "@language": "ru" + }, + { + "@value": "Sage", + "@language": "cy" + }, + { + "@value": "セージ", + "@language": "ja" + }, + { + "@value": "Sábháil", + "@language": "ga" + }, + { + "@value": "Sage", + "@language": "hi" + }, + { + "@value": "工资", + "@language": "zh" + }, + { + "@value": "Sage", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#aromatic", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#salsify", + "rdfs:label": [ + { + "@value": "Salsifis", + "@language": "fr" + }, + { + "@value": "Salsify", + "@language": "en" + }, + { + "@value": "المرتبات", + "@language": "ar" + }, + { + "@value": "Salsify", + "@language": "ku" + }, + { + "@value": "Salsify", + "@language": "es" + }, + { + "@value": "Salsificare", + "@language": "it" + }, + { + "@value": "Salsify", + "@language": "de" + }, + { + "@value": "Salsify", + "@language": "sw" + }, + { + "@value": "Salsificar", + "@language": "pt" + }, + { + "@value": "Salsify", + "@language": "oc" + }, + { + "@value": "Салсифицировать", + "@language": "ru" + }, + { + "@value": "Salsify", + "@language": "cy" + }, + { + "@value": "サルファイ", + "@language": "ja" + }, + { + "@value": "Sailéad a thabhairt", + "@language": "ga" + }, + { + "@value": "सलाद", + "@language": "hi" + }, + { + "@value": "薪金", + "@language": "zh" + }, + { + "@value": "Salsify", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#bean", + "rdfs:label": [ + { + "@value": "Haricot", + "@language": "fr" + }, + { + "@value": "Bean", + "@language": "en" + }, + { + "@value": "Bean", + "@language": "ar" + }, + { + "@value": "Bean", + "@language": "ku" + }, + { + "@value": "Bean", + "@language": "es" + }, + { + "@value": "Fagiolo", + "@language": "it" + }, + { + "@value": "Bohnen", + "@language": "de" + }, + { + "@value": "Bean", + "@language": "sw" + }, + { + "@value": "Feijão", + "@language": "pt" + }, + { + "@value": "Bean", + "@language": "oc" + }, + { + "@value": "Бин", + "@language": "ru" + }, + { + "@value": "Bean", + "@language": "cy" + }, + { + "@value": "ビーンズ", + "@language": "ja" + }, + { + "@value": "Bean Pónaire", + "@language": "ga" + }, + { + "@value": "बीन", + "@language": "hi" + }, + { + "@value": "Bean", + "@language": "zh" + }, + { + "@value": "Bean", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#plum", + "rdfs:label": [ + { + "@value": "Prune", + "@language": "fr" + }, + { + "@value": "Plum", + "@language": "en" + }, + { + "@value": "Plum", + "@language": "ar" + }, + { + "@value": "Plum", + "@language": "ku" + }, + { + "@value": "Plum", + "@language": "es" + }, + { + "@value": "Plum", + "@language": "it" + }, + { + "@value": "Pflaumen", + "@language": "de" + }, + { + "@value": "Plum", + "@language": "sw" + }, + { + "@value": "Plum", + "@language": "pt" + }, + { + "@value": "Plum", + "@language": "oc" + }, + { + "@value": "Слива", + "@language": "ru" + }, + { + "@value": "Plum", + "@language": "cy" + }, + { + "@value": "プラシッド", + "@language": "ja" + }, + { + "@value": "cineál gas: in airde", + "@language": "ga" + }, + { + "@value": "बेर", + "@language": "hi" + }, + { + "@value": "贫民窟", + "@language": "zh" + }, + { + "@value": "Plum", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#fruit", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#pigeon", + "rdfs:label": [ + { + "@value": "Pigeon", + "@language": "fr" + }, + { + "@value": "Pigeon", + "@language": "en" + }, + { + "@value": "Pigeon", + "@language": "ar" + }, + { + "@value": "Pigeon", + "@language": "ku" + }, + { + "@value": "Pigeon", + "@language": "es" + }, + { + "@value": "Pigeon", + "@language": "it" + }, + { + "@value": "Tauben", + "@language": "de" + }, + { + "@value": "Pigeon", + "@language": "sw" + }, + { + "@value": "Pigeon", + "@language": "pt" + }, + { + "@value": "Pigeon", + "@language": "oc" + }, + { + "@value": "Пьежон", + "@language": "ru" + }, + { + "@value": "Pigeon", + "@language": "cy" + }, + { + "@value": "ピジョン", + "@language": "ja" + }, + { + "@value": "riachtanais uisce: measartha", + "@language": "ga" + }, + { + "@value": "कबूतर", + "@language": "hi" + }, + { + "@value": "Pigeon", + "@language": "zh" + }, + { + "@value": "Pigeon", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#poultry", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#basil", + "rdfs:label": [ + { + "@value": "Basilic", + "@language": "fr" + }, + { + "@value": "Basil", + "@language": "en" + }, + { + "@value": "Basil", + "@language": "ar" + }, + { + "@value": "Basil", + "@language": "ku" + }, + { + "@value": "Basil", + "@language": "es" + }, + { + "@value": "Basilico", + "@language": "it" + }, + { + "@value": "Basilikum", + "@language": "de" + }, + { + "@value": "Basil", + "@language": "sw" + }, + { + "@value": "Basílio", + "@language": "pt" + }, + { + "@value": "Basil", + "@language": "oc" + }, + { + "@value": "Басил", + "@language": "ru" + }, + { + "@value": "Basil", + "@language": "cy" + }, + { + "@value": "バジル", + "@language": "ja" + }, + { + "@value": "Basil", + "@language": "ga" + }, + { + "@value": "बेसिल", + "@language": "hi" + }, + { + "@value": "B. 排 物", + "@language": "zh" + }, + { + "@value": "Basil", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#aromatic", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#red-cabbage", + "rdfs:label": [ + { + "@value": "Chou rouge", + "@language": "fr" + }, + { + "@value": "Red cabbage", + "@language": "en" + }, + { + "@value": "سيارة الأجرة الحمراء", + "@language": "ar" + }, + { + "@value": "Red cabbage", + "@language": "ku" + }, + { + "@value": "Caballo rojo", + "@language": "es" + }, + { + "@value": "Cavolo rosso", + "@language": "it" + }, + { + "@value": "Roter Kohl", + "@language": "de" + }, + { + "@value": "Red cabbage", + "@language": "sw" + }, + { + "@value": "Repolho vermelho", + "@language": "pt" + }, + { + "@value": "Red cabbage", + "@language": "oc" + }, + { + "@value": "Красная капуста", + "@language": "ru" + }, + { + "@value": "Red cabbage", + "@language": "cy" + }, + { + "@value": "赤キャベツ", + "@language": "ja" + }, + { + "@value": "Cabáiste dearg", + "@language": "ga" + }, + { + "@value": "लाल गोभी", + "@language": "hi" + }, + { + "@value": "D. 红色", + "@language": "zh" + }, + { + "@value": "Red cabbage", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#cabbage", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#butternut", + "rdfs:label": [ + { + "@value": "Butternut", + "@language": "fr" + }, + { + "@value": "Butternut", + "@language": "en" + }, + { + "@value": "Butternut", + "@language": "ar" + }, + { + "@value": "Butternut", + "@language": "ku" + }, + { + "@value": "Butternut", + "@language": "es" + }, + { + "@value": "Burro", + "@language": "it" + }, + { + "@value": "Butter", + "@language": "de" + }, + { + "@value": "Butternut", + "@language": "sw" + }, + { + "@value": "Castanha", + "@language": "pt" + }, + { + "@value": "Butternut", + "@language": "oc" + }, + { + "@value": "Нотернаут", + "@language": "ru" + }, + { + "@value": "Butternut", + "@language": "cy" + }, + { + "@value": "バターナット", + "@language": "ja" + }, + { + "@value": "bláthanna cumhra: cumhráin", + "@language": "ga" + }, + { + "@value": "बटर्न", + "@language": "hi" + }, + { + "@value": "陪审团", + "@language": "zh" + }, + { + "@value": "Butternut", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#squash", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#celery-branch", + "rdfs:label": [ + { + "@value": "Céleri-branche", + "@language": "fr" + }, + { + "@value": "Celery branch", + "@language": "en" + }, + { + "@value": "فرع الكريات", + "@language": "ar" + }, + { + "@value": "Celery branch", + "@language": "ku" + }, + { + "@value": "Rama de celos", + "@language": "es" + }, + { + "@value": "ramo di cemento", + "@language": "it" + }, + { + "@value": "Cele-Branche", + "@language": "de" + }, + { + "@value": "Celery branch", + "@language": "sw" + }, + { + "@value": "Ramo de celebridades", + "@language": "pt" + }, + { + "@value": "Celery branch", + "@language": "oc" + }, + { + "@value": "Celery филиал", + "@language": "ru" + }, + { + "@value": "Celery branch", + "@language": "cy" + }, + { + "@value": "セルリー支店", + "@language": "ja" + }, + { + "@value": "Craobh na gréine", + "@language": "ga" + }, + { + "@value": "सेलेरी शाखा", + "@language": "hi" + }, + { + "@value": "Celery分支", + "@language": "zh" + }, + { + "@value": "Celery branch", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#coulemelle-mushroom", + "rdfs:label": [ + { + "@value": "Coulemelle", + "@language": "fr" + }, + { + "@value": "Coulemelle mushroom", + "@language": "en" + }, + { + "@value": "Coulemelle mushroom", + "@language": "ar" + }, + { + "@value": "Coulemelle mushroom", + "@language": "ku" + }, + { + "@value": "Setas Coulemelle", + "@language": "es" + }, + { + "@value": "Fungo di Coulemelle", + "@language": "it" + }, + { + "@value": "Pilze von Coulemelle", + "@language": "de" + }, + { + "@value": "Coulemelle mushroom", + "@language": "sw" + }, + { + "@value": "Cogumelo de Coulemelle", + "@language": "pt" + }, + { + "@value": "Coulemelle mushroom", + "@language": "oc" + }, + { + "@value": "Кулемель гриб", + "@language": "ru" + }, + { + "@value": "Coulemelle mushroom", + "@language": "cy" + }, + { + "@value": "コルメレのマッシュルーム", + "@language": "ja" + }, + { + "@value": "Beacán coulemelle", + "@language": "ga" + }, + { + "@value": "Coulemelle मशरूम", + "@language": "hi" + }, + { + "@value": "Coulemelle Mahmoudsh", + "@language": "zh" + }, + { + "@value": "Coulemelle mushroom", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#mushroom", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#apple-cabbage", + "rdfs:label": [ + { + "@value": "Chou pomme", + "@language": "fr" + }, + { + "@value": "Apple cabbage", + "@language": "en" + }, + { + "@value": "مقصورة آبل", + "@language": "ar" + }, + { + "@value": "Apple cabbage", + "@language": "ku" + }, + { + "@value": "Apple cabbage", + "@language": "es" + }, + { + "@value": "Cavo di mela", + "@language": "it" + }, + { + "@value": "Apfelkohl", + "@language": "de" + }, + { + "@value": "Apple cabbage", + "@language": "sw" + }, + { + "@value": "Repolho da Apple", + "@language": "pt" + }, + { + "@value": "Apple cabbage", + "@language": "oc" + }, + { + "@value": "Яблочная капуста", + "@language": "ru" + }, + { + "@value": "Apple cabbage", + "@language": "cy" + }, + { + "@value": "アップルキャベツ", + "@language": "ja" + }, + { + "@value": "Cabáiste Apple", + "@language": "ga" + }, + { + "@value": "सेब", + "@language": "hi" + }, + { + "@value": "A. 补充因果", + "@language": "zh" + }, + { + "@value": "Apple cabbage", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#cabbage", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#cereal", + "rdfs:label": [ + { + "@value": "Céréale", + "@language": "fr" + }, + { + "@value": "Cereal", + "@language": "en" + }, + { + "@value": "Cereal", + "@language": "ar" + }, + { + "@value": "Cereal", + "@language": "ku" + }, + { + "@value": "Cereal", + "@language": "es" + }, + { + "@value": "Cereali", + "@language": "it" + }, + { + "@value": "Getreide", + "@language": "de" + }, + { + "@value": "Cereal", + "@language": "sw" + }, + { + "@value": "Cereais", + "@language": "pt" + }, + { + "@value": "Cereal", + "@language": "oc" + }, + { + "@value": "Сюрре", + "@language": "ru" + }, + { + "@value": "Cereal", + "@language": "cy" + }, + { + "@value": "シリアル", + "@language": "ja" + }, + { + "@value": "Gránaigh", + "@language": "ga" + }, + { + "@value": "अनाज", + "@language": "hi" + }, + { + "@value": "Ceral", + "@language": "zh" + }, + { + "@value": "Cereal", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#savory-groceries", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#canned-fruit", + "rdfs:label": [ + { + "@value": "Fruit en conserve", + "@language": "fr" + }, + { + "@value": "Canned fruit", + "@language": "en" + }, + { + "@value": "الفاكهة المأهولة", + "@language": "ar" + }, + { + "@value": "Canned fruit", + "@language": "ku" + }, + { + "@value": "Fruto enlatado", + "@language": "es" + }, + { + "@value": "Frutta in scatola", + "@language": "it" + }, + { + "@value": "Früchte", + "@language": "de" + }, + { + "@value": "Canned fruit", + "@language": "sw" + }, + { + "@value": "Fruta em conserva", + "@language": "pt" + }, + { + "@value": "Canned fruit", + "@language": "oc" + }, + { + "@value": "Консервированные фрукты", + "@language": "ru" + }, + { + "@value": "Canned fruit", + "@language": "cy" + }, + { + "@value": "缶詰のフルーツ", + "@language": "ja" + }, + { + "@value": "Torthaí stánaithe", + "@language": "ga" + }, + { + "@value": "डिब्बाबंद फल", + "@language": "hi" + }, + { + "@value": "果", + "@language": "zh" + }, + { + "@value": "Canned fruit", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#processed-fruit", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#apples", + "rdfs:label": [ + { + "@value": "Pomme", + "@language": "fr" + }, + { + "@value": "Apples", + "@language": "en" + }, + { + "@value": "Apples", + "@language": "ar" + }, + { + "@value": "Apples", + "@language": "ku" + }, + { + "@value": "Apples", + "@language": "es" + }, + { + "@value": "Mele", + "@language": "it" + }, + { + "@value": "Äpfel", + "@language": "de" + }, + { + "@value": "Apples", + "@language": "sw" + }, + { + "@value": "Maçãs", + "@language": "pt" + }, + { + "@value": "Apples", + "@language": "oc" + }, + { + "@value": "Яблоки", + "@language": "ru" + }, + { + "@value": "Apples", + "@language": "cy" + }, + { + "@value": "アップル", + "@language": "ja" + }, + { + "@value": "Apples", + "@language": "ga" + }, + { + "@value": "सेब", + "@language": "hi" + }, + { + "@value": "附录", + "@language": "zh" + }, + { + "@value": "Apples", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#fruit", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#rutabaga", + "rdfs:label": [ + { + "@value": "Rutabaga", + "@language": "fr" + }, + { + "@value": "Rutabaga", + "@language": "en" + }, + { + "@value": "Rutabaga", + "@language": "ar" + }, + { + "@value": "Rutabaga", + "@language": "ku" + }, + { + "@value": "Rutabaga", + "@language": "es" + }, + { + "@value": "Rutabaga", + "@language": "it" + }, + { + "@value": "Ruanda", + "@language": "de" + }, + { + "@value": "Rutabaga", + "@language": "sw" + }, + { + "@value": "Rutabaga", + "@language": "pt" + }, + { + "@value": "Rutabaga", + "@language": "oc" + }, + { + "@value": "Рутабага", + "@language": "ru" + }, + { + "@value": "Rutabaga", + "@language": "cy" + }, + { + "@value": "ルータバッグ", + "@language": "ja" + }, + { + "@value": "Seirbhís do Chustaiméirí", + "@language": "ga" + }, + { + "@value": "रुटाबागा", + "@language": "hi" + }, + { + "@value": "Rutabaga", + "@language": "zh" + }, + { + "@value": "Rutabaga", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#mousseron", + "rdfs:label": [ + { + "@value": "Mousseron", + "@language": "fr" + }, + { + "@value": "Mousseron", + "@language": "en" + }, + { + "@value": "Mousseron", + "@language": "ar" + }, + { + "@value": "Mousseron", + "@language": "ku" + }, + { + "@value": "Mousseron", + "@language": "es" + }, + { + "@value": "Mousseron", + "@language": "it" + }, + { + "@value": "Mosseron", + "@language": "de" + }, + { + "@value": "Mousseron", + "@language": "sw" + }, + { + "@value": "Mousseron", + "@language": "pt" + }, + { + "@value": "Mousseron", + "@language": "oc" + }, + { + "@value": "Муссерон", + "@language": "ru" + }, + { + "@value": "Mousseron", + "@language": "cy" + }, + { + "@value": "ミセロン", + "@language": "ja" + }, + { + "@value": "riachtanais uisce: measartha", + "@language": "ga" + }, + { + "@value": "मूसरोन", + "@language": "hi" + }, + { + "@value": "Mousseron", + "@language": "zh" + }, + { + "@value": "Mousseron", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#mushroom", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#cauliflower", + "rdfs:label": [ + { + "@value": "Chou-fleur", + "@language": "fr" + }, + { + "@value": "Cauliflower", + "@language": "en" + }, + { + "@value": "Cauliflower", + "@language": "ar" + }, + { + "@value": "Cauliflower", + "@language": "ku" + }, + { + "@value": "Cauliflower", + "@language": "es" + }, + { + "@value": "Cavolfiore", + "@language": "it" + }, + { + "@value": "Blumenkohl", + "@language": "de" + }, + { + "@value": "Cauliflower", + "@language": "sw" + }, + { + "@value": "Colisão de girassol", + "@language": "pt" + }, + { + "@value": "Cauliflower", + "@language": "oc" + }, + { + "@value": "Каулифлауэр", + "@language": "ru" + }, + { + "@value": "Cauliflower", + "@language": "cy" + }, + { + "@value": "カリフラワー", + "@language": "ja" + }, + { + "@value": "Couliflower", + "@language": "ga" + }, + { + "@value": "गोभी", + "@language": "hi" + }, + { + "@value": "Cauli流", + "@language": "zh" + }, + { + "@value": "Cauliflower", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#cabbage", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#chicken", + "rdfs:label": [ + { + "@value": "Poulet", + "@language": "fr" + }, + { + "@value": "Chicken", + "@language": "en" + }, + { + "@value": "الدجاج", + "@language": "ar" + }, + { + "@value": "Chicken", + "@language": "ku" + }, + { + "@value": "Pollo", + "@language": "es" + }, + { + "@value": "Pollo", + "@language": "it" + }, + { + "@value": "Hühnchen", + "@language": "de" + }, + { + "@value": "Chicken", + "@language": "sw" + }, + { + "@value": "Frango", + "@language": "pt" + }, + { + "@value": "Chicken", + "@language": "oc" + }, + { + "@value": "Курица", + "@language": "ru" + }, + { + "@value": "Chicken", + "@language": "cy" + }, + { + "@value": "チキン", + "@language": "ja" + }, + { + "@value": "cineál gas: in airde", + "@language": "ga" + }, + { + "@value": "चिकन", + "@language": "hi" + }, + { + "@value": "Chicken", + "@language": "zh" + }, + { + "@value": "Chicken", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#poultry", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#soup", + "rdfs:label": [ + { + "@value": "Soupe", + "@language": "fr" + }, + { + "@value": "Soup", + "@language": "en" + }, + { + "@value": "الحساء", + "@language": "ar" + }, + { + "@value": "Soup", + "@language": "ku" + }, + { + "@value": "Soup", + "@language": "es" + }, + { + "@value": "Zuppa", + "@language": "it" + }, + { + "@value": "Suppe", + "@language": "de" + }, + { + "@value": "Soup", + "@language": "sw" + }, + { + "@value": "Sopa", + "@language": "pt" + }, + { + "@value": "Soup", + "@language": "oc" + }, + { + "@value": "Суп", + "@language": "ru" + }, + { + "@value": "Soup", + "@language": "cy" + }, + { + "@value": "スープ", + "@language": "ja" + }, + { + "@value": "Soup", + "@language": "ga" + }, + { + "@value": "सूप", + "@language": "hi" + }, + { + "@value": "准备", + "@language": "zh" + }, + { + "@value": "Soup", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#processed-vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#fruit", + "rdfs:label": [ + { + "@value": "Fruit", + "@language": "fr" + }, + { + "@value": "Fruit", + "@language": "en" + }, + { + "@value": "Fruit", + "@language": "ar" + }, + { + "@value": "Fruit", + "@language": "ku" + }, + { + "@value": "Frutas", + "@language": "es" + }, + { + "@value": "Frutta", + "@language": "it" + }, + { + "@value": "Früchte", + "@language": "de" + }, + { + "@value": "Fruit", + "@language": "sw" + }, + { + "@value": "Frutas", + "@language": "pt" + }, + { + "@value": "Fruit", + "@language": "oc" + }, + { + "@value": "Фрукты", + "@language": "ru" + }, + { + "@value": "Fruit", + "@language": "cy" + }, + { + "@value": "フルーツ", + "@language": "ja" + }, + { + "@value": "Torthaí torthaí", + "@language": "ga" + }, + { + "@value": "फल", + "@language": "hi" + }, + { + "@value": "诉讼", + "@language": "zh" + }, + { + "@value": "Fruit", + "@language": "ca" + } + ], + "dfc-p:specialize": "undefined", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#confectionery", + "rdfs:label": [ + { + "@value": "Confiserie", + "@language": "fr" + }, + { + "@value": "Confectionery", + "@language": "en" + }, + { + "@value": "العدوى", + "@language": "ar" + }, + { + "@value": "Confectionery", + "@language": "ku" + }, + { + "@value": "Confección", + "@language": "es" + }, + { + "@value": "Confezioni", + "@language": "it" + }, + { + "@value": "Konfidenz", + "@language": "de" + }, + { + "@value": "Confectionery", + "@language": "sw" + }, + { + "@value": "Confeitaria", + "@language": "pt" + }, + { + "@value": "Confectionery", + "@language": "oc" + }, + { + "@value": "Кондитерские изделия", + "@language": "ru" + }, + { + "@value": "Confectionery", + "@language": "cy" + }, + { + "@value": "コンピューション", + "@language": "ja" + }, + { + "@value": "An tIomlán", + "@language": "ga" + }, + { + "@value": "कन्फेक्शनरी", + "@language": "hi" + }, + { + "@value": "惩罚", + "@language": "zh" + }, + { + "@value": "Confectionery", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#sweet-groceries", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#oil", + "rdfs:label": [ + { + "@value": "Huile", + "@language": "fr" + }, + { + "@value": "Oil", + "@language": "en" + }, + { + "@value": "النفط", + "@language": "ar" + }, + { + "@value": "Oil", + "@language": "ku" + }, + { + "@value": "Aceite", + "@language": "es" + }, + { + "@value": "Olio", + "@language": "it" + }, + { + "@value": "Öl", + "@language": "de" + }, + { + "@value": "Oil", + "@language": "sw" + }, + { + "@value": "Óleo de óleo", + "@language": "pt" + }, + { + "@value": "Oil", + "@language": "oc" + }, + { + "@value": "Нефть", + "@language": "ru" + }, + { + "@value": "Oil", + "@language": "cy" + }, + { + "@value": "オイル", + "@language": "ja" + }, + { + "@value": "Ola agus Ola", + "@language": "ga" + }, + { + "@value": "तेल", + "@language": "hi" + }, + { + "@value": "石油", + "@language": "zh" + }, + { + "@value": "Oil", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#savory-groceries", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#fresh-cream", + "rdfs:label": [ + { + "@value": "Crème Fraîche", + "@language": "fr" + }, + { + "@value": "Fresh cream", + "@language": "en" + }, + { + "@value": "كريمة جديدة", + "@language": "ar" + }, + { + "@value": "Fresh cream", + "@language": "ku" + }, + { + "@value": "Crema fresca", + "@language": "es" + }, + { + "@value": "Crema fresca", + "@language": "it" + }, + { + "@value": "Frische Creme", + "@language": "de" + }, + { + "@value": "Fresh cream", + "@language": "sw" + }, + { + "@value": "Creme fresco", + "@language": "pt" + }, + { + "@value": "Fresh cream", + "@language": "oc" + }, + { + "@value": "Свежий крем", + "@language": "ru" + }, + { + "@value": "Fresh cream", + "@language": "cy" + }, + { + "@value": "フレッシュクリーム", + "@language": "ja" + }, + { + "@value": "uachtar reoite", + "@language": "ga" + }, + { + "@value": "ताजा क्रीम", + "@language": "hi" + }, + { + "@value": "F. 淡水的创造", + "@language": "zh" + }, + { + "@value": "Fresh cream", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#cow-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#sheep-dairy-product", + "rdfs:label": [ + { + "@value": "Produits laitiers de brebis", + "@language": "fr" + }, + { + "@value": "Sheep dairy product", + "@language": "en" + }, + { + "@value": "منتجات الألبان", + "@language": "ar" + }, + { + "@value": "Sheep dairy product", + "@language": "ku" + }, + { + "@value": "Producto lácteo de ovejas", + "@language": "es" + }, + { + "@value": "Prodotti lattiero-caseari", + "@language": "it" + }, + { + "@value": "Schafmilchprodukt", + "@language": "de" + }, + { + "@value": "Sheep dairy product", + "@language": "sw" + }, + { + "@value": "Produtos lácteos de ovelhas", + "@language": "pt" + }, + { + "@value": "Sheep dairy product", + "@language": "oc" + }, + { + "@value": "Овцы молочный продукт", + "@language": "ru" + }, + { + "@value": "Sheep dairy product", + "@language": "cy" + }, + { + "@value": "羊の酪農場プロダクト", + "@language": "ja" + }, + { + "@value": "Táirge déiríochta caorach", + "@language": "ga" + }, + { + "@value": "भेड़ डेयरी उत्पाद", + "@language": "hi" + }, + { + "@value": "她佩尔·达利产品", + "@language": "zh" + }, + { + "@value": "Sheep dairy product", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#mandarin", + "rdfs:label": [ + { + "@value": "Mandarine", + "@language": "fr" + }, + { + "@value": "Mandarin", + "@language": "en" + }, + { + "@value": "Mandarin", + "@language": "ar" + }, + { + "@value": "Mandarin", + "@language": "ku" + }, + { + "@value": "Mandarin", + "@language": "es" + }, + { + "@value": "Mandarino", + "@language": "it" + }, + { + "@value": "Mandarinen", + "@language": "de" + }, + { + "@value": "Mandarin", + "@language": "sw" + }, + { + "@value": "Mandarim", + "@language": "pt" + }, + { + "@value": "Mandarin", + "@language": "oc" + }, + { + "@value": "Мандарин", + "@language": "ru" + }, + { + "@value": "Mandarin", + "@language": "cy" + }, + { + "@value": "マンダリン", + "@language": "ja" + }, + { + "@value": "cineál gas: in airde", + "@language": "ga" + }, + { + "@value": "मंदारिन", + "@language": "hi" + }, + { + "@value": "曼达尔", + "@language": "zh" + }, + { + "@value": "Mandarin", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#fruit", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#garlic", + "rdfs:label": [ + { + "@value": "Ail", + "@language": "fr" + }, + { + "@value": "Garlic", + "@language": "en" + }, + { + "@value": "الثوم", + "@language": "ar" + }, + { + "@value": "Garlic", + "@language": "ku" + }, + { + "@value": "Ajo", + "@language": "es" + }, + { + "@value": "Aglio", + "@language": "it" + }, + { + "@value": "Knoblauch", + "@language": "de" + }, + { + "@value": "Garlic", + "@language": "sw" + }, + { + "@value": "Alho", + "@language": "pt" + }, + { + "@value": "Garlic", + "@language": "oc" + }, + { + "@value": "Гарлик", + "@language": "ru" + }, + { + "@value": "Garlic", + "@language": "cy" + }, + { + "@value": "ガーリック", + "@language": "ja" + }, + { + "@value": "Gairleoige", + "@language": "ga" + }, + { + "@value": "लहसुन", + "@language": "hi" + }, + { + "@value": "加 法", + "@language": "zh" + }, + { + "@value": "Garlic", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#yogurt-with-fruits", + "rdfs:label": [ + { + "@value": "Yaourt aux fruits", + "@language": "fr" + }, + { + "@value": "Yogurt with fruits", + "@language": "en" + }, + { + "@value": "الزبادي مع الفواكه", + "@language": "ar" + }, + { + "@value": "Yogurt with fruits", + "@language": "ku" + }, + { + "@value": "Yogur con frutas", + "@language": "es" + }, + { + "@value": "Yogurt con frutti", + "@language": "it" + }, + { + "@value": "Joghurt mit Früchten", + "@language": "de" + }, + { + "@value": "Yogurt with fruits", + "@language": "sw" + }, + { + "@value": "Iogurte com frutas", + "@language": "pt" + }, + { + "@value": "Yogurt with fruits", + "@language": "oc" + }, + { + "@value": "Йогурт с фруктами", + "@language": "ru" + }, + { + "@value": "Yogurt with fruits", + "@language": "cy" + }, + { + "@value": "フルーツとヨーグルト", + "@language": "ja" + }, + { + "@value": "Yogurt le torthaí", + "@language": "ga" + }, + { + "@value": "फलों के साथ दही", + "@language": "hi" + }, + { + "@value": "取得成果的明天", + "@language": "zh" + }, + { + "@value": "Yogurt with fruits", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#cow-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#butter", + "rdfs:label": [ + { + "@value": "Beurre", + "@language": "fr" + }, + { + "@value": "Butter", + "@language": "en" + }, + { + "@value": "Butter", + "@language": "ar" + }, + { + "@value": "Butter", + "@language": "ku" + }, + { + "@value": "Butter", + "@language": "es" + }, + { + "@value": "Butter", + "@language": "it" + }, + { + "@value": "Butter", + "@language": "de" + }, + { + "@value": "Butter", + "@language": "sw" + }, + { + "@value": "Butter", + "@language": "pt" + }, + { + "@value": "Butter", + "@language": "oc" + }, + { + "@value": "Буттер", + "@language": "ru" + }, + { + "@value": "Butter", + "@language": "cy" + }, + { + "@value": "バター", + "@language": "ja" + }, + { + "@value": "Im", + "@language": "ga" + }, + { + "@value": "मक्खन", + "@language": "hi" + }, + { + "@value": "但", + "@language": "zh" + }, + { + "@value": "Butter", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#cow-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#poultry", + "rdfs:label": [ + { + "@value": "Volaille", + "@language": "fr" + }, + { + "@value": "Poultry", + "@language": "en" + }, + { + "@value": "Poultry", + "@language": "ar" + }, + { + "@value": "Poultry", + "@language": "ku" + }, + { + "@value": "Poultry", + "@language": "es" + }, + { + "@value": "Pollame", + "@language": "it" + }, + { + "@value": "Geflügel", + "@language": "de" + }, + { + "@value": "Poultry", + "@language": "sw" + }, + { + "@value": "Aves de capoeira", + "@language": "pt" + }, + { + "@value": "Poultry", + "@language": "oc" + }, + { + "@value": "птица", + "@language": "ru" + }, + { + "@value": "Poultry", + "@language": "cy" + }, + { + "@value": "パンフレット", + "@language": "ja" + }, + { + "@value": "Linnte snámha", + "@language": "ga" + }, + { + "@value": "पोल्ट्री", + "@language": "hi" + }, + { + "@value": "导 言", + "@language": "zh" + }, + { + "@value": "Poultry", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#meat-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#fishery-product", + "rdfs:label": [ + { + "@value": "Produit de la pêche", + "@language": "fr" + }, + { + "@value": "Fishery product", + "@language": "en" + }, + { + "@value": "منتجات الأسماك", + "@language": "ar" + }, + { + "@value": "Fishery product", + "@language": "ku" + }, + { + "@value": "Producto pesquero", + "@language": "es" + }, + { + "@value": "Prodotti della pesca", + "@language": "it" + }, + { + "@value": "Fischereierzeugnisse", + "@language": "de" + }, + { + "@value": "Fishery product", + "@language": "sw" + }, + { + "@value": "Produto de pesca", + "@language": "pt" + }, + { + "@value": "Fishery product", + "@language": "oc" + }, + { + "@value": "Рыболовный продукт", + "@language": "ru" + }, + { + "@value": "Fishery product", + "@language": "cy" + }, + { + "@value": "漁業製品", + "@language": "ja" + }, + { + "@value": "Táirge iascaigh", + "@language": "ga" + }, + { + "@value": "मत्स्य उत्पाद", + "@language": "hi" + }, + { + "@value": "渔业产品", + "@language": "zh" + }, + { + "@value": "Fishery product", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#meat-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#porcini", + "rdfs:label": [ + { + "@value": "Cèpe", + "@language": "fr" + }, + { + "@value": "Porcini", + "@language": "en" + }, + { + "@value": "Porcini", + "@language": "ar" + }, + { + "@value": "Porcini", + "@language": "ku" + }, + { + "@value": "Porcini", + "@language": "es" + }, + { + "@value": "Porcini", + "@language": "it" + }, + { + "@value": "Porcine", + "@language": "de" + }, + { + "@value": "Porcini", + "@language": "sw" + }, + { + "@value": "Porcini", + "@language": "pt" + }, + { + "@value": "Porcini", + "@language": "oc" + }, + { + "@value": "Корзини", + "@language": "ru" + }, + { + "@value": "Porcini", + "@language": "cy" + }, + { + "@value": "ポルチーニ", + "@language": "ja" + }, + { + "@value": "Próiseas an Chúrsa", + "@language": "ga" + }, + { + "@value": "पोर्किनी", + "@language": "hi" + }, + { + "@value": "Porcini", + "@language": "zh" + }, + { + "@value": "Porcini", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#mushroom", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#bluefoot-mushroom", + "rdfs:label": [ + { + "@value": "Pied-bleu", + "@language": "fr" + }, + { + "@value": "Bluefoot mushroom", + "@language": "en" + }, + { + "@value": "الفطر الأزرق القدم", + "@language": "ar" + }, + { + "@value": "Bluefoot mushroom", + "@language": "ku" + }, + { + "@value": "Setas de pie azul", + "@language": "es" + }, + { + "@value": "Fungo di Bluefoot", + "@language": "it" + }, + { + "@value": "Blauer Pilz", + "@language": "de" + }, + { + "@value": "Bluefoot mushroom", + "@language": "sw" + }, + { + "@value": "Cogumelo de pé azul", + "@language": "pt" + }, + { + "@value": "Bluefoot mushroom", + "@language": "oc" + }, + { + "@value": "Голубой гриб", + "@language": "ru" + }, + { + "@value": "Bluefoot mushroom", + "@language": "cy" + }, + { + "@value": "青足マッシュルーム", + "@language": "ja" + }, + { + "@value": "Beacán Bluefoot", + "@language": "ga" + }, + { + "@value": "ब्लूफुट मशरूम", + "@language": "hi" + }, + { + "@value": "蓝线", + "@language": "zh" + }, + { + "@value": "Bluefoot mushroom", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#mushroom", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#dairy-dessert", + "rdfs:label": [ + { + "@value": "Dessert lacté", + "@language": "fr" + }, + { + "@value": "Dairy dessert", + "@language": "en" + }, + { + "@value": "الحلوى", + "@language": "ar" + }, + { + "@value": "Dairy dessert", + "@language": "ku" + }, + { + "@value": "Postre lácteo", + "@language": "es" + }, + { + "@value": "Dessert di latticini", + "@language": "it" + }, + { + "@value": "Dairy Dessert", + "@language": "de" + }, + { + "@value": "Dairy dessert", + "@language": "sw" + }, + { + "@value": "sobremesa de leite", + "@language": "pt" + }, + { + "@value": "Dairy dessert", + "@language": "oc" + }, + { + "@value": "Молочный десерт", + "@language": "ru" + }, + { + "@value": "Dairy dessert", + "@language": "cy" + }, + { + "@value": "デザート", + "@language": "ja" + }, + { + "@value": "Milseog Déiríochta", + "@language": "ga" + }, + { + "@value": "डेयरी डेसर्ट", + "@language": "hi" + }, + { + "@value": "Dairy desert", + "@language": "zh" + }, + { + "@value": "Dairy dessert", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#cow-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#leek", + "rdfs:label": [ + { + "@value": "Poireau", + "@language": "fr" + }, + { + "@value": "Leek", + "@language": "en" + }, + { + "@value": "ليك", + "@language": "ar" + }, + { + "@value": "Leek", + "@language": "ku" + }, + { + "@value": "Leek", + "@language": "es" + }, + { + "@value": "Leek", + "@language": "it" + }, + { + "@value": "Leek", + "@language": "de" + }, + { + "@value": "Leek", + "@language": "sw" + }, + { + "@value": "Leek.", + "@language": "pt" + }, + { + "@value": "Leek", + "@language": "oc" + }, + { + "@value": "Лик", + "@language": "ru" + }, + { + "@value": "Leek", + "@language": "cy" + }, + { + "@value": "リーク", + "@language": "ja" + }, + { + "@value": "Le haghaidh tuilleadh eolais", + "@language": "ga" + }, + { + "@value": "लीक", + "@language": "hi" + }, + { + "@value": "Leek", + "@language": "zh" + }, + { + "@value": "Leek", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#beetroot", + "rdfs:label": [ + { + "@value": "Betterave rouge", + "@language": "fr" + }, + { + "@value": "Beetroot", + "@language": "en" + }, + { + "@value": "Beetroot", + "@language": "ar" + }, + { + "@value": "Beetroot", + "@language": "ku" + }, + { + "@value": "Beetroot", + "@language": "es" + }, + { + "@value": "Razza di barbabietola", + "@language": "it" + }, + { + "@value": "Rinder", + "@language": "de" + }, + { + "@value": "Beetroot", + "@language": "sw" + }, + { + "@value": "Beterraba", + "@language": "pt" + }, + { + "@value": "Beetroot", + "@language": "oc" + }, + { + "@value": "Битроот", + "@language": "ru" + }, + { + "@value": "Beetroot", + "@language": "cy" + }, + { + "@value": "ビートルート", + "@language": "ja" + }, + { + "@value": "bláthanna cumhra: cumhráin", + "@language": "ga" + }, + { + "@value": "चुकंदर", + "@language": "hi" + }, + { + "@value": "B. 特 卫", + "@language": "zh" + }, + { + "@value": "Beetroot", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#lentils", + "rdfs:label": [ + { + "@value": "Lentilles", + "@language": "fr" + }, + { + "@value": "Lentils", + "@language": "en" + }, + { + "@value": "Lentils", + "@language": "ar" + }, + { + "@value": "Lentils", + "@language": "ku" + }, + { + "@value": "Lentils", + "@language": "es" + }, + { + "@value": "Lenticchie", + "@language": "it" + }, + { + "@value": "Linsen", + "@language": "de" + }, + { + "@value": "Lentils", + "@language": "sw" + }, + { + "@value": "Lentilhas", + "@language": "pt" + }, + { + "@value": "Lentils", + "@language": "oc" + }, + { + "@value": "Лентилс", + "@language": "ru" + }, + { + "@value": "Lentils", + "@language": "cy" + }, + { + "@value": "レンティーユ", + "@language": "ja" + }, + { + "@value": "Toir agus Crainn", + "@language": "ga" + }, + { + "@value": "लेन्टिल", + "@language": "hi" + }, + { + "@value": "学 历", + "@language": "zh" + }, + { + "@value": "Lentils", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#dried-vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#medlar", + "rdfs:label": [ + { + "@value": "Nèfle", + "@language": "fr" + }, + { + "@value": "Medlar", + "@language": "en" + }, + { + "@value": "Medlar", + "@language": "ar" + }, + { + "@value": "Medlar", + "@language": "ku" + }, + { + "@value": "Medlar", + "@language": "es" + }, + { + "@value": "Medaglia", + "@language": "it" + }, + { + "@value": "Medaille", + "@language": "de" + }, + { + "@value": "Medlar", + "@language": "sw" + }, + { + "@value": "Medalha.", + "@language": "pt" + }, + { + "@value": "Medlar", + "@language": "oc" + }, + { + "@value": "Медлар", + "@language": "ru" + }, + { + "@value": "Medlar", + "@language": "cy" + }, + { + "@value": "メデラー", + "@language": "ja" + }, + { + "@value": "taiseachas aeir: fliuch", + "@language": "ga" + }, + { + "@value": "मेडलर", + "@language": "hi" + }, + { + "@value": "Medlar", + "@language": "zh" + }, + { + "@value": "Medlar", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#nut", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#chive", + "rdfs:label": [ + { + "@value": "Ciboulette", + "@language": "fr" + }, + { + "@value": "Chive", + "@language": "en" + }, + { + "@value": "تشيفي", + "@language": "ar" + }, + { + "@value": "Chive", + "@language": "ku" + }, + { + "@value": "Chive", + "@language": "es" + }, + { + "@value": "Chi.", + "@language": "it" + }, + { + "@value": "Chicorée", + "@language": "de" + }, + { + "@value": "Chive", + "@language": "sw" + }, + { + "@value": "Chive", + "@language": "pt" + }, + { + "@value": "Chive", + "@language": "oc" + }, + { + "@value": "Чиви", + "@language": "ru" + }, + { + "@value": "Chive", + "@language": "cy" + }, + { + "@value": "シーブ", + "@language": "ja" + }, + { + "@value": "Léim", + "@language": "ga" + }, + { + "@value": "चीव", + "@language": "hi" + }, + { + "@value": "事实", + "@language": "zh" + }, + { + "@value": "Chive", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#aromatic", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#apricot", + "rdfs:label": [ + { + "@value": "Abricot", + "@language": "fr" + }, + { + "@value": "Apricot", + "@language": "en" + }, + { + "@value": "Apricot", + "@language": "ar" + }, + { + "@value": "Apricot", + "@language": "ku" + }, + { + "@value": "Apricot", + "@language": "es" + }, + { + "@value": "Apricottura", + "@language": "it" + }, + { + "@value": "Aprikosen", + "@language": "de" + }, + { + "@value": "Apricot", + "@language": "sw" + }, + { + "@value": "A damasco", + "@language": "pt" + }, + { + "@value": "Apricot", + "@language": "oc" + }, + { + "@value": "Абрикос", + "@language": "ru" + }, + { + "@value": "Apricot", + "@language": "cy" + }, + { + "@value": "アプライコット", + "@language": "ja" + }, + { + "@value": "taiseachas aeir: fliuch", + "@language": "ga" + }, + { + "@value": "खुबानी", + "@language": "hi" + }, + { + "@value": "注", + "@language": "zh" + }, + { + "@value": "Apricot", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#fruit", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#orange", + "rdfs:label": [ + { + "@value": "Orange", + "@language": "fr" + }, + { + "@value": "Orange", + "@language": "en" + }, + { + "@value": "غريب", + "@language": "ar" + }, + { + "@value": "Orange", + "@language": "ku" + }, + { + "@value": "Naranja", + "@language": "es" + }, + { + "@value": "Arancione", + "@language": "it" + }, + { + "@value": "Orange", + "@language": "de" + }, + { + "@value": "Orange", + "@language": "sw" + }, + { + "@value": "Laranja", + "@language": "pt" + }, + { + "@value": "Orange", + "@language": "oc" + }, + { + "@value": "Оранжевый", + "@language": "ru" + }, + { + "@value": "Orange", + "@language": "cy" + }, + { + "@value": "オレンジ", + "@language": "ja" + }, + { + "@value": "Oráiste na mBan", + "@language": "ga" + }, + { + "@value": "नारंगी", + "@language": "hi" + }, + { + "@value": "安排", + "@language": "zh" + }, + { + "@value": "Orange", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#fruit", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#old-variety-tomato", + "rdfs:label": [ + { + "@value": "Tomate ancienne", + "@language": "fr" + }, + { + "@value": "Old variety tomato", + "@language": "en" + }, + { + "@value": "الطماطم القديمة", + "@language": "ar" + }, + { + "@value": "Old variety tomato", + "@language": "ku" + }, + { + "@value": "Tomate de gran variedad", + "@language": "es" + }, + { + "@value": "Pomodoro di varietà vecchia", + "@language": "it" + }, + { + "@value": "Alte Sorte Tomaten", + "@language": "de" + }, + { + "@value": "Old variety tomato", + "@language": "sw" + }, + { + "@value": "Tomate de variedade antiga", + "@language": "pt" + }, + { + "@value": "Old variety tomato", + "@language": "oc" + }, + { + "@value": "Помидор старого разнообразия", + "@language": "ru" + }, + { + "@value": "Old variety tomato", + "@language": "cy" + }, + { + "@value": "古い品種トマト", + "@language": "ja" + }, + { + "@value": "Trátaí sean-éagsúlacht", + "@language": "ga" + }, + { + "@value": "पुरानी विविधता टमाटर", + "@language": "hi" + }, + { + "@value": "老年人", + "@language": "zh" + }, + { + "@value": "Old variety tomato", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#tomato", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#walnut", + "rdfs:label": [ + { + "@value": "Noix", + "@language": "fr" + }, + { + "@value": "Walnut", + "@language": "en" + }, + { + "@value": "والنت", + "@language": "ar" + }, + { + "@value": "Walnut", + "@language": "ku" + }, + { + "@value": "Walnut", + "@language": "es" + }, + { + "@value": "Noce", + "@language": "it" + }, + { + "@value": "Walnuss", + "@language": "de" + }, + { + "@value": "Walnut", + "@language": "sw" + }, + { + "@value": "Noz", + "@language": "pt" + }, + { + "@value": "Walnut", + "@language": "oc" + }, + { + "@value": "орех", + "@language": "ru" + }, + { + "@value": "Walnut", + "@language": "cy" + }, + { + "@value": "クルミ", + "@language": "ja" + }, + { + "@value": "cineál gas: in airde", + "@language": "ga" + }, + { + "@value": "अखरोट", + "@language": "hi" + }, + { + "@value": "沃尔夫", + "@language": "zh" + }, + { + "@value": "Walnut", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#nut", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#shellfish", + "rdfs:label": [ + { + "@value": "Crustacé", + "@language": "fr" + }, + { + "@value": "Shellfish", + "@language": "en" + }, + { + "@value": "Shellfish", + "@language": "ar" + }, + { + "@value": "Shellfish", + "@language": "ku" + }, + { + "@value": "Shellfish", + "@language": "es" + }, + { + "@value": "Pesce di conchiglia", + "@language": "it" + }, + { + "@value": "Schalenfrüchte", + "@language": "de" + }, + { + "@value": "Shellfish", + "@language": "sw" + }, + { + "@value": "Peixe-marinho", + "@language": "pt" + }, + { + "@value": "Shellfish", + "@language": "oc" + }, + { + "@value": "Shellfish", + "@language": "ru" + }, + { + "@value": "Shellfish", + "@language": "cy" + }, + { + "@value": "シェルフィッシュ", + "@language": "ja" + }, + { + "@value": "An tSliogán", + "@language": "ga" + }, + { + "@value": "शेलफिश", + "@language": "hi" + }, + { + "@value": "D. 鱼", + "@language": "zh" + }, + { + "@value": "Shellfish", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#fishery-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#sheepfoot-mushroom", + "rdfs:label": [ + { + "@value": "Pied de mouton", + "@language": "fr" + }, + { + "@value": "Sheepfoot mushroom", + "@language": "en" + }, + { + "@value": "فطرة شيبفوت", + "@language": "ar" + }, + { + "@value": "Sheepfoot mushroom", + "@language": "ku" + }, + { + "@value": "Setas de pies de pie", + "@language": "es" + }, + { + "@value": "Fungo di pecora", + "@language": "it" + }, + { + "@value": "Pilze aus Schaffuß", + "@language": "de" + }, + { + "@value": "Sheepfoot mushroom", + "@language": "sw" + }, + { + "@value": "Cogumelo de ovelhas", + "@language": "pt" + }, + { + "@value": "Sheepfoot mushroom", + "@language": "oc" + }, + { + "@value": "Шефффут гриб", + "@language": "ru" + }, + { + "@value": "Sheepfoot mushroom", + "@language": "cy" + }, + { + "@value": "羊足のマッシュルーム", + "@language": "ja" + }, + { + "@value": "Beacán caorach", + "@language": "ga" + }, + { + "@value": "शीपफुट मशरूम", + "@language": "hi" + }, + { + "@value": "她穿梭汽车", + "@language": "zh" + }, + { + "@value": "Sheepfoot mushroom", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#mushroom", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#prune", + "rdfs:label": [ + { + "@value": "Pruneau", + "@language": "fr" + }, + { + "@value": "Prune", + "@language": "en" + }, + { + "@value": "Prune", + "@language": "ar" + }, + { + "@value": "Prune", + "@language": "ku" + }, + { + "@value": "Prune", + "@language": "es" + }, + { + "@value": "Prune", + "@language": "it" + }, + { + "@value": "Prun", + "@language": "de" + }, + { + "@value": "Prune", + "@language": "sw" + }, + { + "@value": "Prune", + "@language": "pt" + }, + { + "@value": "Prune", + "@language": "oc" + }, + { + "@value": "Прун", + "@language": "ru" + }, + { + "@value": "Prune", + "@language": "cy" + }, + { + "@value": "プルーン", + "@language": "ja" + }, + { + "@value": "tréimhse de chuid eile: aon", + "@language": "ga" + }, + { + "@value": "प्रणो", + "@language": "hi" + }, + { + "@value": "Prune", + "@language": "zh" + }, + { + "@value": "Prune", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#fruit", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#simmering-meat", + "rdfs:label": [ + { + "@value": "Viande à mijoter", + "@language": "fr" + }, + { + "@value": "Simmering meat", + "@language": "en" + }, + { + "@value": "لحم متحرك", + "@language": "ar" + }, + { + "@value": "Simmering meat", + "@language": "ku" + }, + { + "@value": "Carne inmersa", + "@language": "es" + }, + { + "@value": "Carni materiche", + "@language": "it" + }, + { + "@value": "Simmerndes Fleisch", + "@language": "de" + }, + { + "@value": "Simmering meat", + "@language": "sw" + }, + { + "@value": "Carne de Simmering", + "@language": "pt" + }, + { + "@value": "Simmering meat", + "@language": "oc" + }, + { + "@value": "Погружение мяса", + "@language": "ru" + }, + { + "@value": "Simmering meat", + "@language": "cy" + }, + { + "@value": "肉を煮る", + "@language": "ja" + }, + { + "@value": "Feoil suathaireachta", + "@language": "ga" + }, + { + "@value": "simmering मांस", + "@language": "hi" + }, + { + "@value": "A. 活性肉", + "@language": "zh" + }, + { + "@value": "Simmering meat", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#beef", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#dried-vegetable", + "rdfs:label": [ + { + "@value": "Légume sec", + "@language": "fr" + }, + { + "@value": "Dried vegetable", + "@language": "en" + }, + { + "@value": "الخضروات الجافة", + "@language": "ar" + }, + { + "@value": "Dried vegetable", + "@language": "ku" + }, + { + "@value": "vegetal seca", + "@language": "es" + }, + { + "@value": "Ortaggi secchi", + "@language": "it" + }, + { + "@value": "Getrocknetes Gemüse", + "@language": "de" + }, + { + "@value": "Dried vegetable", + "@language": "sw" + }, + { + "@value": "Vegetação seca", + "@language": "pt" + }, + { + "@value": "Dried vegetable", + "@language": "oc" + }, + { + "@value": "Сухой овощ", + "@language": "ru" + }, + { + "@value": "Dried vegetable", + "@language": "cy" + }, + { + "@value": "乾燥野菜", + "@language": "ja" + }, + { + "@value": "Glasraí triomaithe", + "@language": "ga" + }, + { + "@value": "सूखे सब्जी", + "@language": "hi" + }, + { + "@value": "Dried蔬菜", + "@language": "zh" + }, + { + "@value": "Dried vegetable", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#lamb", + "rdfs:label": [ + { + "@value": "Agneau", + "@language": "fr" + }, + { + "@value": "Lamb", + "@language": "en" + }, + { + "@value": "Lamb", + "@language": "ar" + }, + { + "@value": "Lamb", + "@language": "ku" + }, + { + "@value": "Cordero", + "@language": "es" + }, + { + "@value": "Agnello", + "@language": "it" + }, + { + "@value": "Lammfell", + "@language": "de" + }, + { + "@value": "Lamb", + "@language": "sw" + }, + { + "@value": "Cordeiro", + "@language": "pt" + }, + { + "@value": "Lamb", + "@language": "oc" + }, + { + "@value": "Ламб", + "@language": "ru" + }, + { + "@value": "Lamb", + "@language": "cy" + }, + { + "@value": "ラム", + "@language": "ja" + }, + { + "@value": "An tAthrán", + "@language": "ga" + }, + { + "@value": "मेमने", + "@language": "hi" + }, + { + "@value": "Lamb", + "@language": "zh" + }, + { + "@value": "Lamb", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#meat-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#fruit-juice", + "rdfs:label": [ + { + "@value": "Jus de fruit", + "@language": "fr" + }, + { + "@value": "Fruit juice", + "@language": "en" + }, + { + "@value": "عصير الفواكه", + "@language": "ar" + }, + { + "@value": "Fruit juice", + "@language": "ku" + }, + { + "@value": "Zumo de frutas", + "@language": "es" + }, + { + "@value": "Succo di frutta", + "@language": "it" + }, + { + "@value": "Fruchtsaft", + "@language": "de" + }, + { + "@value": "Fruit juice", + "@language": "sw" + }, + { + "@value": "Sumo de fruta", + "@language": "pt" + }, + { + "@value": "Fruit juice", + "@language": "oc" + }, + { + "@value": "Фруктовый сок", + "@language": "ru" + }, + { + "@value": "Fruit juice", + "@language": "cy" + }, + { + "@value": "フルーツジュース", + "@language": "ja" + }, + { + "@value": "Sú torthaí", + "@language": "ga" + }, + { + "@value": "फलों का रस", + "@language": "hi" + }, + { + "@value": "弗朗西斯·理学", + "@language": "zh" + }, + { + "@value": "Fruit juice", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#soft-drink", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#goat-mature-cheese", + "rdfs:label": [ + { + "@value": "Fromage affinés", + "@language": "fr" + }, + { + "@value": "Goat Mature cheese", + "@language": "en" + }, + { + "@value": "جبنة الجوزة", + "@language": "ar" + }, + { + "@value": "Goat Mature cheese", + "@language": "ku" + }, + { + "@value": "Queso maduro", + "@language": "es" + }, + { + "@value": "Formaggi di carne", + "@language": "it" + }, + { + "@value": "Ziegenkäse", + "@language": "de" + }, + { + "@value": "Goat Mature cheese", + "@language": "sw" + }, + { + "@value": "Goat Queijo de milho", + "@language": "pt" + }, + { + "@value": "Goat Mature cheese", + "@language": "oc" + }, + { + "@value": "Goat Зрелый сыр", + "@language": "ru" + }, + { + "@value": "Goat Mature cheese", + "@language": "cy" + }, + { + "@value": "Goat 成熟した チーズ", + "@language": "ja" + }, + { + "@value": "Goat cáis Aibí", + "@language": "ga" + }, + { + "@value": "बकरी परिपक्व पनीर", + "@language": "hi" + }, + { + "@value": "古特·米洛斯", + "@language": "zh" + }, + { + "@value": "Goat Mature cheese", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#goat-dairy-product", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#biscuit", + "rdfs:label": [ + { + "@value": "Biscuit", + "@language": "fr" + }, + { + "@value": "Biscuit", + "@language": "en" + }, + { + "@value": "بسكويت", + "@language": "ar" + }, + { + "@value": "Biscuit", + "@language": "ku" + }, + { + "@value": "Biscuit", + "@language": "es" + }, + { + "@value": "Biscotto", + "@language": "it" + }, + { + "@value": "Biscuit", + "@language": "de" + }, + { + "@value": "Biscuit", + "@language": "sw" + }, + { + "@value": "Bola de bola", + "@language": "pt" + }, + { + "@value": "Biscuit", + "@language": "oc" + }, + { + "@value": "Бисквит", + "@language": "ru" + }, + { + "@value": "Biscuit", + "@language": "cy" + }, + { + "@value": "ビスケット", + "@language": "ja" + }, + { + "@value": "An Roinn", + "@language": "ga" + }, + { + "@value": "बिस्किट", + "@language": "hi" + }, + { + "@value": "B. 拜 门", + "@language": "zh" + }, + { + "@value": "Biscuit", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#sweet-groceries", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#pumpkin", + "rdfs:label": [ + { + "@value": "Potiron", + "@language": "fr" + }, + { + "@value": "Pumpkin", + "@language": "en" + }, + { + "@value": "Pumpkin", + "@language": "ar" + }, + { + "@value": "Pumpkin", + "@language": "ku" + }, + { + "@value": "Pumpkin", + "@language": "es" + }, + { + "@value": "Zucca", + "@language": "it" + }, + { + "@value": "Kürbis", + "@language": "de" + }, + { + "@value": "Pumpkin", + "@language": "sw" + }, + { + "@value": "Abóbora", + "@language": "pt" + }, + { + "@value": "Pumpkin", + "@language": "oc" + }, + { + "@value": "Тыква", + "@language": "ru" + }, + { + "@value": "Pumpkin", + "@language": "cy" + }, + { + "@value": "パンプキン", + "@language": "ja" + }, + { + "@value": "Pumpkin", + "@language": "ga" + }, + { + "@value": "कद्दू", + "@language": "hi" + }, + { + "@value": "Pumpkin", + "@language": "zh" + }, + { + "@value": "Pumpkin", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#squash", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#mesclun", + "rdfs:label": [ + { + "@value": "Mesclun", + "@language": "fr" + }, + { + "@value": "Mesclun", + "@language": "en" + }, + { + "@value": "Mesclun", + "@language": "ar" + }, + { + "@value": "Mesclun", + "@language": "ku" + }, + { + "@value": "Mesclun", + "@language": "es" + }, + { + "@value": "Mesclud", + "@language": "it" + }, + { + "@value": "Mesclun", + "@language": "de" + }, + { + "@value": "Mesclun", + "@language": "sw" + }, + { + "@value": "Mecânica", + "@language": "pt" + }, + { + "@value": "Mesclun", + "@language": "oc" + }, + { + "@value": "Месклаун", + "@language": "ru" + }, + { + "@value": "Mesclun", + "@language": "cy" + }, + { + "@value": "メスクラン", + "@language": "ja" + }, + { + "@value": "riachtanais uisce: measartha", + "@language": "ga" + }, + { + "@value": "मेस्क्लून", + "@language": "hi" + }, + { + "@value": "梅迪松", + "@language": "zh" + }, + { + "@value": "Mesclun", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#salad", + "@type": "dfc-p:ProductType" + }, + { + "@id": "http://static.datafoodconsortium.org/data/productTypes.rdf#carrot", + "rdfs:label": [ + { + "@value": "Carotte", + "@language": "fr" + }, + { + "@value": "Carrot", + "@language": "en" + }, + { + "@value": "Carrot", + "@language": "ar" + }, + { + "@value": "Carrot", + "@language": "ku" + }, + { + "@value": "Carrot", + "@language": "es" + }, + { + "@value": "Carrot", + "@language": "it" + }, + { + "@value": "Karotten", + "@language": "de" + }, + { + "@value": "Carrot", + "@language": "sw" + }, + { + "@value": "Cenoura", + "@language": "pt" + }, + { + "@value": "Carrot", + "@language": "oc" + }, + { + "@value": "Морковь", + "@language": "ru" + }, + { + "@value": "Carrot", + "@language": "cy" + }, + { + "@value": "キャロット", + "@language": "ja" + }, + { + "@value": "Seirbhís do Chustaiméirí", + "@language": "ga" + }, + { + "@value": "गाजर", + "@language": "hi" + }, + { + "@value": "加 罗", + "@language": "zh" + }, + { + "@value": "Carrot", + "@language": "ca" + } + ], + "dfc-p:specialize": "http://static.datafoodconsortium.org/data/productTypes.rdf#vegetable", + "@type": "dfc-p:ProductType" + } + ] +} diff --git a/ontology/medicalTypes.json b/ontology/medicalTypes.json new file mode 100644 index 000000000..8e7e84689 --- /dev/null +++ b/ontology/medicalTypes.json @@ -0,0 +1,2114 @@ +{ + "@context": { + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "dfc-b": "http://static.datafoodconsortium.org/ontologies/DFC_BusinessOntology.owl#", + "dfc-p": "http://static.datafoodconsortium.org/ontologies/DFC_ProductOntology.owl#", + "dfc-t": "http://static.datafoodconsortium.org/ontologies/DFC_TechnicalOntology.owl#", + "dfc-u": "http://static.datafoodconsortium.org/data/units.rdf#", + "dfc-p:specialize": { + "@type": "@id" + } + }, + "@graph": [ + { + "@id": "https://medical/data/medicalTypes.rdf#gas-mask", + "rdfs:label": [ + { + "@value": "Gas Mask", + "@language": "en" + }, + { + "@value": "Gas Mask", + "@language": "ar" + }, + { + "@value": "Gas Mask", + "@language": "ku" + }, + { + "@value": "Mascara de gas", + "@language": "es" + }, + { + "@value": "Maschera di gas", + "@language": "it" + }, + { + "@value": "Gasmaske", + "@language": "de" + }, + { + "@value": "Gas Mask", + "@language": "sw" + }, + { + "@value": "Máscara de gás", + "@language": "pt" + }, + { + "@value": "Gas Mask", + "@language": "oc" + }, + { + "@value": "Газовая маска", + "@language": "ru" + }, + { + "@value": "Gas Mask", + "@language": "cy" + }, + { + "@value": "ガスマスク", + "@language": "ja" + }, + { + "@value": "Gáis Masc", + "@language": "ga" + }, + { + "@value": "गैस मास्क", + "@language": "hi" + }, + { + "@value": "Gs Mask", + "@language": "zh" + }, + { + "@value": "Masque de gaz", + "@language": "fr" + }, + { + "@value": "Gas Mask", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#body-protection", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://medical/data/medicalTypes.rdf#gas-mask-filter", + "rdfs:label": [ + { + "@value": "Gas Mask Filter", + "@language": "en" + }, + { + "@value": "مقصورة ماسك", + "@language": "ar" + }, + { + "@value": "Gas Mask Filter", + "@language": "ku" + }, + { + "@value": "Filtro de máscara de gas", + "@language": "es" + }, + { + "@value": "Filtro della maschera di gas", + "@language": "it" + }, + { + "@value": "Gasmaske Filter", + "@language": "de" + }, + { + "@value": "Gas Mask Filter", + "@language": "sw" + }, + { + "@value": "Filtro de máscara de gás", + "@language": "pt" + }, + { + "@value": "Gas Mask Filter", + "@language": "oc" + }, + { + "@value": "Газовая Маска Фильтр", + "@language": "ru" + }, + { + "@value": "Gas Mask Filter", + "@language": "cy" + }, + { + "@value": "ガス マスク フィルター", + "@language": "ja" + }, + { + "@value": "Gáis Scagaire Masc", + "@language": "ga" + }, + { + "@value": "गैस मास्क फ़िल्टर", + "@language": "hi" + }, + { + "@value": "Gas Mask Filter", + "@language": "zh" + }, + { + "@value": "Filtre à masque de gaz", + "@language": "fr" + }, + { + "@value": "Gas Mask Filter", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#body-protection", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://medical/data/medicalTypes.rdf#bandages", + "rdfs:label": [ + { + "@value": "Bandages", + "@language": "en" + }, + { + "@value": "الفرق", + "@language": "ar" + }, + { + "@value": "Bandages", + "@language": "ku" + }, + { + "@value": "Bandages", + "@language": "es" + }, + { + "@value": "Bandiere", + "@language": "it" + }, + { + "@value": "Bandagen", + "@language": "de" + }, + { + "@value": "Bandages", + "@language": "sw" + }, + { + "@value": "Bandas", + "@language": "pt" + }, + { + "@value": "Bandages", + "@language": "oc" + }, + { + "@value": "Бандажи", + "@language": "ru" + }, + { + "@value": "Bandages", + "@language": "cy" + }, + { + "@value": "バンド", + "@language": "ja" + }, + { + "@value": "Bannaí", + "@language": "ga" + }, + { + "@value": "बंद", + "@language": "hi" + }, + { + "@value": "优点", + "@language": "zh" + }, + { + "@value": "Bandages", + "@language": "fr" + }, + { + "@value": "Bandages", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#bandages", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://medical/data/medicalTypes.rdf#gauze-wrap", + "rdfs:label": [ + { + "@value": "Gauze Wrap", + "@language": "en" + }, + { + "@value": "Gauze Wrap", + "@language": "ar" + }, + { + "@value": "Gauze Wrap", + "@language": "ku" + }, + { + "@value": "Gauze Wrap", + "@language": "es" + }, + { + "@value": "Avvolto di Gauze", + "@language": "it" + }, + { + "@value": "Würmer", + "@language": "de" + }, + { + "@value": "Gauze Wrap", + "@language": "sw" + }, + { + "@value": "Envoltório de Gauze", + "@language": "pt" + }, + { + "@value": "Gauze Wrap", + "@language": "oc" + }, + { + "@value": "Gauze Обертка", + "@language": "ru" + }, + { + "@value": "Gauze Wrap", + "@language": "cy" + }, + { + "@value": "ガーゼラップ", + "@language": "ja" + }, + { + "@value": "cliceáil grianghraf a mhéadú", + "@language": "ga" + }, + { + "@value": "Gauze लपेटें", + "@language": "hi" + }, + { + "@value": "Guze Wrap", + "@language": "zh" + }, + { + "@value": "Gauze Wrap", + "@language": "fr" + }, + { + "@value": "Gauze Wrap", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#bandages", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://medical/data/medicalTypes.rdf#gauze-pad", + "rdfs:label": [ + { + "@value": "Gauze Pad", + "@language": "en" + }, + { + "@value": "Gauze Pad", + "@language": "ar" + }, + { + "@value": "Gauze Pad", + "@language": "ku" + }, + { + "@value": "Gauze Pad", + "@language": "es" + }, + { + "@value": "Pad di Gauze", + "@language": "it" + }, + { + "@value": "STRUKTUR-Pad", + "@language": "de" + }, + { + "@value": "Gauze Pad", + "@language": "sw" + }, + { + "@value": "Gauze Pad", + "@language": "pt" + }, + { + "@value": "Gauze Pad", + "@language": "oc" + }, + { + "@value": "Gauze Пад", + "@language": "ru" + }, + { + "@value": "Gauze Pad", + "@language": "cy" + }, + { + "@value": "ガーゼパッド", + "@language": "ja" + }, + { + "@value": "Monaróir a tháirgtear: Uimh", + "@language": "ga" + }, + { + "@value": "गौज पैड", + "@language": "hi" + }, + { + "@value": "Gauze Pad", + "@language": "zh" + }, + { + "@value": "Gauze Pad", + "@language": "fr" + }, + { + "@value": "Gauze Pad", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#bandages", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://medical/data/medicalTypes.rdf#nonstick-pad", + "rdfs:label": [ + { + "@value": "Nonstick Pad", + "@language": "en" + }, + { + "@value": "بدل غير ثابت", + "@language": "ar" + }, + { + "@value": "Nonstick Pad", + "@language": "ku" + }, + { + "@value": "Pasillo de pie", + "@language": "es" + }, + { + "@value": "Pad antiaderente", + "@language": "it" + }, + { + "@value": "Antihaft-Pad", + "@language": "de" + }, + { + "@value": "Nonstick Pad", + "@language": "sw" + }, + { + "@value": "Almofada de Nonstick", + "@language": "pt" + }, + { + "@value": "Nonstick Pad", + "@language": "oc" + }, + { + "@value": "Nonstick Pad", + "@language": "ru" + }, + { + "@value": "Nonstick Pad", + "@language": "cy" + }, + { + "@value": "ノンスティックパッド", + "@language": "ja" + }, + { + "@value": "Pad Neamh-Glasáil", + "@language": "ga" + }, + { + "@value": "नॉनस्टिक पैड", + "@language": "hi" + }, + { + "@value": "非物质帕德", + "@language": "zh" + }, + { + "@value": "Cadeau antiadhésif", + "@language": "fr" + }, + { + "@value": "Nonstick Pad", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#bandages", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://medical/data/medicalTypes.rdf#triangle-bandage", + "rdfs:label": [ + { + "@value": "Triangle Bandage", + "@language": "en" + }, + { + "@value": "المثلث", + "@language": "ar" + }, + { + "@value": "Triangle Bandage", + "@language": "ku" + }, + { + "@value": "Bandage de triángulo", + "@language": "es" + }, + { + "@value": "Bandatura del triangolo", + "@language": "it" + }, + { + "@value": "Triangle Bandage", + "@language": "de" + }, + { + "@value": "Triangle Bandage", + "@language": "sw" + }, + { + "@value": "Bandagem de triângulo", + "@language": "pt" + }, + { + "@value": "Triangle Bandage", + "@language": "oc" + }, + { + "@value": "Треугольный бандаж", + "@language": "ru" + }, + { + "@value": "Triangle Bandage", + "@language": "cy" + }, + { + "@value": "三角形の包帯", + "@language": "ja" + }, + { + "@value": "Banna Triantán", + "@language": "ga" + }, + { + "@value": "त्रिभुज पट्टी", + "@language": "hi" + }, + { + "@value": "特里格勒·巴迪", + "@language": "zh" + }, + { + "@value": "Bande de triangles", + "@language": "fr" + }, + { + "@value": "Triangle Bandage", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#bandages", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://medical/data/medicalTypes.rdf#wound-closure-strip", + "rdfs:label": [ + { + "@value": "Wound Closure Strip", + "@language": "en" + }, + { + "@value": "قطاع الإغلاق", + "@language": "ar" + }, + { + "@value": "Wound Closure Strip", + "@language": "ku" + }, + { + "@value": "Wound Closure Strip", + "@language": "es" + }, + { + "@value": "Striscia di chiusura in tessuto", + "@language": "it" + }, + { + "@value": "Wound Closing Streifen", + "@language": "de" + }, + { + "@value": "Wound Closure Strip", + "@language": "sw" + }, + { + "@value": "Faixa de fechamento de ferida", + "@language": "pt" + }, + { + "@value": "Wound Closure Strip", + "@language": "oc" + }, + { + "@value": "Шум закрытие Strip", + "@language": "ru" + }, + { + "@value": "Wound Closure Strip", + "@language": "cy" + }, + { + "@value": "傷の閉鎖のストリップ", + "@language": "ja" + }, + { + "@value": "Clúdaigh Wound Stiall", + "@language": "ga" + }, + { + "@value": "घाव बंद करने वाली पट्टी", + "@language": "hi" + }, + { + "@value": "妇女的关闭", + "@language": "zh" + }, + { + "@value": "Bande de fermeture laine", + "@language": "fr" + }, + { + "@value": "Wound Closure Strip", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#bandages", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://medical/data/medicalTypes.rdf#paper-tape", + "rdfs:label": [ + { + "@value": "Paper Tape", + "@language": "en" + }, + { + "@value": "Paper Tape", + "@language": "ar" + }, + { + "@value": "Paper Tape", + "@language": "ku" + }, + { + "@value": "Tapa de papel", + "@language": "es" + }, + { + "@value": "Nastro di carta", + "@language": "it" + }, + { + "@value": "Papierband", + "@language": "de" + }, + { + "@value": "Paper Tape", + "@language": "sw" + }, + { + "@value": "Fita de papel", + "@language": "pt" + }, + { + "@value": "Paper Tape", + "@language": "oc" + }, + { + "@value": "Бумажная лента", + "@language": "ru" + }, + { + "@value": "Paper Tape", + "@language": "cy" + }, + { + "@value": "ペーパー テープ", + "@language": "ja" + }, + { + "@value": "Páipéar Téip", + "@language": "ga" + }, + { + "@value": "पेपर टेप", + "@language": "hi" + }, + { + "@value": "论文", + "@language": "zh" + }, + { + "@value": "Paper Tape", + "@language": "fr" + }, + { + "@value": "Paper Tape", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#tape", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://medical/data/medicalTypes.rdf#plastic-tape", + "rdfs:label": [ + { + "@value": "Plastic Tape", + "@language": "en" + }, + { + "@value": "بلاستيك تاب", + "@language": "ar" + }, + { + "@value": "Plastic Tape", + "@language": "ku" + }, + { + "@value": "Tapa de plástico", + "@language": "es" + }, + { + "@value": "Nastro di plastica", + "@language": "it" + }, + { + "@value": "Kunststoffband", + "@language": "de" + }, + { + "@value": "Plastic Tape", + "@language": "sw" + }, + { + "@value": "Fita de plástico", + "@language": "pt" + }, + { + "@value": "Plastic Tape", + "@language": "oc" + }, + { + "@value": "Пластиковые ленты", + "@language": "ru" + }, + { + "@value": "Plastic Tape", + "@language": "cy" + }, + { + "@value": "プラスチック テープ", + "@language": "ja" + }, + { + "@value": "Plaisteach Téip", + "@language": "ga" + }, + { + "@value": "प्लास्टिक टेप", + "@language": "hi" + }, + { + "@value": "Pl弹 Tape", + "@language": "zh" + }, + { + "@value": "Tape en plastique", + "@language": "fr" + }, + { + "@value": "Plastic Tape", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#tape", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://medical/data/medicalTypes.rdf#examination-gloves", + "rdfs:label": [ + { + "@value": "Examination Gloves", + "@language": "en" + }, + { + "@value": "امتحانات", + "@language": "ar" + }, + { + "@value": "Examination Gloves", + "@language": "ku" + }, + { + "@value": "Globos de examen", + "@language": "es" + }, + { + "@value": "Guanti di esame", + "@language": "it" + }, + { + "@value": "Prüfungshandschuhe", + "@language": "de" + }, + { + "@value": "Examination Gloves", + "@language": "sw" + }, + { + "@value": "Luvas de Exame", + "@language": "pt" + }, + { + "@value": "Examination Gloves", + "@language": "oc" + }, + { + "@value": "Обследование Перчатки", + "@language": "ru" + }, + { + "@value": "Examination Gloves", + "@language": "cy" + }, + { + "@value": "検査手袋", + "@language": "ja" + }, + { + "@value": "Lámhleabhair na Scrúduithe", + "@language": "ga" + }, + { + "@value": "परीक्षा दस्ताने", + "@language": "hi" + }, + { + "@value": "考试", + "@language": "zh" + }, + { + "@value": "Gloves d ' examen", + "@language": "fr" + }, + { + "@value": "Examination Gloves", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#gloves", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://medical/data/medicalTypes.rdf#stick-on-bandage", + "rdfs:label": [ + { + "@value": "Stick-on Bandage", + "@language": "en" + }, + { + "@value": "ستيك على باندج", + "@language": "ar" + }, + { + "@value": "Stick-on Bandage", + "@language": "ku" + }, + { + "@value": "Bandage de palo", + "@language": "es" + }, + { + "@value": "Bandage", + "@language": "it" + }, + { + "@value": "Stick-on Bandage", + "@language": "de" + }, + { + "@value": "Stick-on Bandage", + "@language": "sw" + }, + { + "@value": "Bandagem de Stick-on", + "@language": "pt" + }, + { + "@value": "Stick-on Bandage", + "@language": "oc" + }, + { + "@value": "Стик-на Bandage", + "@language": "ru" + }, + { + "@value": "Stick-on Bandage", + "@language": "cy" + }, + { + "@value": "スティックオン包帯", + "@language": "ja" + }, + { + "@value": "Bandage bata ar", + "@language": "ga" + }, + { + "@value": "स्टिक-ऑन बैंडेज", + "@language": "hi" + }, + { + "@value": "圣克-昂", + "@language": "zh" + }, + { + "@value": "Bande adhésive", + "@language": "fr" + }, + { + "@value": "Stick-on Bandage", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#bandages", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://medical/data/medicalTypes.rdf#saline-solution", + "rdfs:label": [ + { + "@value": "Saline Solution", + "@language": "en" + }, + { + "@value": "الحلبة", + "@language": "ar" + }, + { + "@value": "Saline Solution", + "@language": "ku" + }, + { + "@value": "Saline Solution", + "@language": "es" + }, + { + "@value": "Soluzione Saline", + "@language": "it" + }, + { + "@value": "Saline Lösung", + "@language": "de" + }, + { + "@value": "Saline Solution", + "@language": "sw" + }, + { + "@value": "Solução Saline", + "@language": "pt" + }, + { + "@value": "Saline Solution", + "@language": "oc" + }, + { + "@value": "Saline решение", + "@language": "ru" + }, + { + "@value": "Saline Solution", + "@language": "cy" + }, + { + "@value": "サラインソリューション", + "@language": "ja" + }, + { + "@value": "Saline Réiteach", + "@language": "ga" + }, + { + "@value": "सैलिन समाधान", + "@language": "hi" + }, + { + "@value": "电话:", + "@language": "zh" + }, + { + "@value": "Saline Solution", + "@language": "fr" + }, + { + "@value": "Saline Solution", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#fluid", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://medical/data/medicalTypes.rdf#antibiotic-ointment", + "rdfs:label": [ + { + "@value": "Antibiotic Ointment", + "@language": "en" + }, + { + "@value": "مضادات حيوية", + "@language": "ar" + }, + { + "@value": "Antibiotic Ointment", + "@language": "ku" + }, + { + "@value": "Ungüento antibiótico", + "@language": "es" + }, + { + "@value": "Unguento antibiotico", + "@language": "it" + }, + { + "@value": "Antibiotische Salbe", + "@language": "de" + }, + { + "@value": "Antibiotic Ointment", + "@language": "sw" + }, + { + "@value": "Pomada antibiótica", + "@language": "pt" + }, + { + "@value": "Antibiotic Ointment", + "@language": "oc" + }, + { + "@value": "Антибиотический мазь", + "@language": "ru" + }, + { + "@value": "Antibiotic Ointment", + "@language": "cy" + }, + { + "@value": "抗生物質軟膏", + "@language": "ja" + }, + { + "@value": "An bhfuil a fhios agat na buntáistí a bhaineann...", + "@language": "ga" + }, + { + "@value": "एंटीबायोटिक Ointment", + "@language": "hi" + }, + { + "@value": "反生物组织", + "@language": "zh" + }, + { + "@value": "Ointment antibiotique", + "@language": "fr" + }, + { + "@value": "Antibiotic Ointment", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#medicine", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://medical/data/medicalTypes.rdf#anti-hemorrhagic-agent", + "rdfs:label": [ + { + "@value": "Anti-hemorrhagic Agent", + "@language": "en" + }, + { + "@value": "Anti-hemorrhagic العميل", + "@language": "ar" + }, + { + "@value": "Anti-hemorrhagic Agent", + "@language": "ku" + }, + { + "@value": "Anti-hemorrágico Agente", + "@language": "es" + }, + { + "@value": "Anti-emorragico Agente", + "@language": "it" + }, + { + "@value": "Antihemorrhagic Agent", + "@language": "de" + }, + { + "@value": "Anti-hemorrhagic Agent", + "@language": "sw" + }, + { + "@value": "Anti-hemorrágica Agente", + "@language": "pt" + }, + { + "@value": "Anti-hemorrhagic Agent", + "@language": "oc" + }, + { + "@value": "Анти-хеморрхагическая Агент", + "@language": "ru" + }, + { + "@value": "Anti-hemorrhagic Agent", + "@language": "cy" + }, + { + "@value": "反hemorrhagic エージェント", + "@language": "ja" + }, + { + "@value": "Frith-sceimhlitheoireacht Gníomhaire", + "@language": "ga" + }, + { + "@value": "विरोधी hemorrhagic एजेंट", + "@language": "hi" + }, + { + "@value": "防治腹泻 智 利", + "@language": "zh" + }, + { + "@value": "Anti-hemorrhagic Agent", + "@language": "fr" + }, + { + "@value": "Anti-hemorrhagic Agent", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#fluids", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://medical/data/medicalTypes.rdf#sunblock", + "rdfs:label": [ + { + "@value": "Sunblock", + "@language": "en" + }, + { + "@value": "Sunblock", + "@language": "ar" + }, + { + "@value": "Sunblock", + "@language": "ku" + }, + { + "@value": "Sunblock", + "@language": "es" + }, + { + "@value": "Blocco solare", + "@language": "it" + }, + { + "@value": "Sunblock", + "@language": "de" + }, + { + "@value": "Sunblock", + "@language": "sw" + }, + { + "@value": "Bloqueio solar", + "@language": "pt" + }, + { + "@value": "Sunblock", + "@language": "oc" + }, + { + "@value": "Солнцеблок", + "@language": "ru" + }, + { + "@value": "Sunblock", + "@language": "cy" + }, + { + "@value": "サンブロック", + "@language": "ja" + }, + { + "@value": "ghrian nochtadh: leath-scáth", + "@language": "ga" + }, + { + "@value": "सनब्लॉक", + "@language": "hi" + }, + { + "@value": "日 锁", + "@language": "zh" + }, + { + "@value": "Sunblock", + "@language": "fr" + }, + { + "@value": "Sunblock", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#body-protection", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://medical/data/medicalTypes.rdf#bandage-shears", + "rdfs:label": [ + { + "@value": "Bandage Shears", + "@language": "en" + }, + { + "@value": "مفارز الزينة", + "@language": "ar" + }, + { + "@value": "Bandage Shears", + "@language": "ku" + }, + { + "@value": "Bandage Shears", + "@language": "es" + }, + { + "@value": "Cesoie di fasciatura", + "@language": "it" + }, + { + "@value": "Bandage Shears", + "@language": "de" + }, + { + "@value": "Bandage Shears", + "@language": "sw" + }, + { + "@value": "Bandage Shears", + "@language": "pt" + }, + { + "@value": "Bandage Shears", + "@language": "oc" + }, + { + "@value": "Бандаж Шэрс", + "@language": "ru" + }, + { + "@value": "Bandage Shears", + "@language": "cy" + }, + { + "@value": "包帯のせん断", + "@language": "ja" + }, + { + "@value": "Seirbhís do Chustaiméirí", + "@language": "ga" + }, + { + "@value": "बैंडेज शीयर", + "@language": "hi" + }, + { + "@value": "Bandage Shears", + "@language": "zh" + }, + { + "@value": "Bandage Shears", + "@language": "fr" + }, + { + "@value": "Bandage Shears", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#bandages", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://medical/data/medicalTypes.rdf#tweezers", + "rdfs:label": [ + { + "@value": "Tweezers", + "@language": "en" + }, + { + "@value": "Tweezers", + "@language": "ar" + }, + { + "@value": "Tweezers", + "@language": "ku" + }, + { + "@value": "Tweezers", + "@language": "es" + }, + { + "@value": "Pinzette", + "@language": "it" + }, + { + "@value": "Pinzette", + "@language": "de" + }, + { + "@value": "Tweezers", + "@language": "sw" + }, + { + "@value": "Apertos", + "@language": "pt" + }, + { + "@value": "Tweezers", + "@language": "oc" + }, + { + "@value": "Твеезерс", + "@language": "ru" + }, + { + "@value": "Tweezers", + "@language": "cy" + }, + { + "@value": "ピンセット", + "@language": "ja" + }, + { + "@value": "Seirbhís do Chustaiméirí", + "@language": "ga" + }, + { + "@value": "चिमटी", + "@language": "hi" + }, + { + "@value": "Tweezers", + "@language": "zh" + }, + { + "@value": "Tweezers", + "@language": "fr" + }, + { + "@value": "Tweezers", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#medical-tools", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://medical/data/medicalTypes.rdf#protein-bar", + "rdfs:label": [ + { + "@value": "Protein Bar", + "@language": "en" + }, + { + "@value": "Bar Protein", + "@language": "ar" + }, + { + "@value": "Protein Bar", + "@language": "ku" + }, + { + "@value": "Protein Bar", + "@language": "es" + }, + { + "@value": "Bar della proteina", + "@language": "it" + }, + { + "@value": "Protein Bar", + "@language": "de" + }, + { + "@value": "Protein Bar", + "@language": "sw" + }, + { + "@value": "Barra de Proteína", + "@language": "pt" + }, + { + "@value": "Protein Bar", + "@language": "oc" + }, + { + "@value": "Protein Бар", + "@language": "ru" + }, + { + "@value": "Protein Bar", + "@language": "cy" + }, + { + "@value": "プロテインバー", + "@language": "ja" + }, + { + "@value": "Táirgí do bhfianaise faoi stiúir glan", + "@language": "ga" + }, + { + "@value": "प्रोटीन बार", + "@language": "hi" + }, + { + "@value": "B. Protein Bar", + "@language": "zh" + }, + { + "@value": "Protein Bar", + "@language": "fr" + }, + { + "@value": "Protein Bar", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#energy", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://medical/data/medicalTypes.rdf#bandanna", + "rdfs:label": [ + { + "@value": "Bandanna", + "@language": "en" + }, + { + "@value": "باندانا", + "@language": "ar" + }, + { + "@value": "Bandanna", + "@language": "ku" + }, + { + "@value": "Bandanna", + "@language": "es" + }, + { + "@value": "Bandanna", + "@language": "it" + }, + { + "@value": "Das ist eine gute Idee.", + "@language": "de" + }, + { + "@value": "Bandanna", + "@language": "sw" + }, + { + "@value": "Ligação", + "@language": "pt" + }, + { + "@value": "Bandanna", + "@language": "oc" + }, + { + "@value": "Банданна", + "@language": "ru" + }, + { + "@value": "Bandanna", + "@language": "cy" + }, + { + "@value": "バンドナ", + "@language": "ja" + }, + { + "@value": "An tSraith Shinsearach", + "@language": "ga" + }, + { + "@value": "बांदाना", + "@language": "hi" + }, + { + "@value": "班达纳", + "@language": "zh" + }, + { + "@value": "Bandanna", + "@language": "fr" + }, + { + "@value": "Bandanna", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#body-protection", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://medical/data/medicalTypes.rdf#water-bottle", + "rdfs:label": [ + { + "@value": "Water Bottle", + "@language": "en" + }, + { + "@value": "بؤرة المياه", + "@language": "ar" + }, + { + "@value": "Water Bottle", + "@language": "ku" + }, + { + "@value": "Botella de agua", + "@language": "es" + }, + { + "@value": "Bottiglia di acqua", + "@language": "it" + }, + { + "@value": "Wasserflasche", + "@language": "de" + }, + { + "@value": "Water Bottle", + "@language": "sw" + }, + { + "@value": "Garrafa de água", + "@language": "pt" + }, + { + "@value": "Water Bottle", + "@language": "oc" + }, + { + "@value": "Бутылка воды", + "@language": "ru" + }, + { + "@value": "Water Bottle", + "@language": "cy" + }, + { + "@value": "ウォーターボトル", + "@language": "ja" + }, + { + "@value": "Buidéal Uisce", + "@language": "ga" + }, + { + "@value": "पानी की बोतल", + "@language": "hi" + }, + { + "@value": "水土", + "@language": "zh" + }, + { + "@value": "Bouteille d ' eau", + "@language": "fr" + }, + { + "@value": "Water Bottle", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#fluids", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://medical/data/medicalTypes.rdf#ice-pack", + "rdfs:label": [ + { + "@value": "Ice Pack", + "@language": "en" + }, + { + "@value": "Ice Pack", + "@language": "ar" + }, + { + "@value": "Ice Pack", + "@language": "ku" + }, + { + "@value": "Paquete de hielo", + "@language": "es" + }, + { + "@value": "Pacchetto ghiaccio", + "@language": "it" + }, + { + "@value": "Eispaket", + "@language": "de" + }, + { + "@value": "Ice Pack", + "@language": "sw" + }, + { + "@value": "Pacote de gelo", + "@language": "pt" + }, + { + "@value": "Ice Pack", + "@language": "oc" + }, + { + "@value": "Ледовый пакет", + "@language": "ru" + }, + { + "@value": "Ice Pack", + "@language": "cy" + }, + { + "@value": "アイスパック", + "@language": "ja" + }, + { + "@value": "Pacáiste Oighear", + "@language": "ga" + }, + { + "@value": "आइस पैक", + "@language": "hi" + }, + { + "@value": "Ice Pack", + "@language": "zh" + }, + { + "@value": "Ice Pack", + "@language": "fr" + }, + { + "@value": "Ice Pack", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#anti-inflamatory", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://medical/data/medicalTypes.rdf#messenger-bag", + "rdfs:label": [ + { + "@value": "Messenger Bag", + "@language": "en" + }, + { + "@value": "Messenger Bag", + "@language": "ar" + }, + { + "@value": "Messenger Bag", + "@language": "ku" + }, + { + "@value": "Messenger Bag", + "@language": "es" + }, + { + "@value": "Borsa Messenger", + "@language": "it" + }, + { + "@value": "Tasche für die Reise", + "@language": "de" + }, + { + "@value": "Messenger Bag", + "@language": "sw" + }, + { + "@value": "Bolsa de mensageiro", + "@language": "pt" + }, + { + "@value": "Messenger Bag", + "@language": "oc" + }, + { + "@value": "Сумка Messenger", + "@language": "ru" + }, + { + "@value": "Messenger Bag", + "@language": "cy" + }, + { + "@value": "メッセンジャーバッグ", + "@language": "ja" + }, + { + "@value": "mála láimhe", + "@language": "ga" + }, + { + "@value": "मेसेंजर बैग", + "@language": "hi" + }, + { + "@value": "Messenger Bag", + "@language": "zh" + }, + { + "@value": "Messenger Bag", + "@language": "fr" + }, + { + "@value": "Messenger Bag", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#bag", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://medical/data/medicalTypes.rdf#glucose-tablets", + "rdfs:label": [ + { + "@value": "Glucose tablets", + "@language": "en" + }, + { + "@value": "أقراص غلوكوز", + "@language": "ar" + }, + { + "@value": "Glucose tablets", + "@language": "ku" + }, + { + "@value": "tabletas de glucosa", + "@language": "es" + }, + { + "@value": "Compresse di glucosio", + "@language": "it" + }, + { + "@value": "Glucose-Tabletten", + "@language": "de" + }, + { + "@value": "Glucose tablets", + "@language": "sw" + }, + { + "@value": "Comprimidos de glicose", + "@language": "pt" + }, + { + "@value": "Glucose tablets", + "@language": "oc" + }, + { + "@value": "Глюкозы таблетки", + "@language": "ru" + }, + { + "@value": "Glucose tablets", + "@language": "cy" + }, + { + "@value": "グルコース錠", + "@language": "ja" + }, + { + "@value": "táibléad glúcóis", + "@language": "ga" + }, + { + "@value": "ग्लूकोज़ टैबलेट", + "@language": "hi" + }, + { + "@value": "Glucose 表", + "@language": "zh" + }, + { + "@value": "comprimés de Glucose", + "@language": "fr" + }, + { + "@value": "Glucose tablets", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#energy", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://medical/data/medicalTypes.rdf#liquid-antacid-water-mixture", + "rdfs:label": [ + { + "@value": "Liquid Antacid Water Mixture", + "@language": "en" + }, + { + "@value": "تركيبة المياه السائلة", + "@language": "ar" + }, + { + "@value": "Liquid Antacid Water Mixture", + "@language": "ku" + }, + { + "@value": "Mezcla de agua de antácidos líquidos", + "@language": "es" + }, + { + "@value": "Miscela ad acqua liquido", + "@language": "it" + }, + { + "@value": "Flüssige Antacid Wassermischung", + "@language": "de" + }, + { + "@value": "Liquid Antacid Water Mixture", + "@language": "sw" + }, + { + "@value": "Misturador de água antagonida líquido", + "@language": "pt" + }, + { + "@value": "Liquid Antacid Water Mixture", + "@language": "oc" + }, + { + "@value": "Жидкость Antacid Water Mixture", + "@language": "ru" + }, + { + "@value": "Liquid Antacid Water Mixture", + "@language": "cy" + }, + { + "@value": "液体のAntacid水混合物", + "@language": "ja" + }, + { + "@value": "Meascthóir Uisce Antacid leachtach", + "@language": "ga" + }, + { + "@value": "तरल Antacid जल मिश्रण", + "@language": "hi" + }, + { + "@value": "液体Atacid Water Mixture", + "@language": "zh" + }, + { + "@value": "Mélange d ' eau antacidique liquide", + "@language": "fr" + }, + { + "@value": "Liquid Antacid Water Mixture", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#fluids", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://medical/data/medicalTypes.rdf#re-hydration-mixture", + "rdfs:label": [ + { + "@value": "Re-hydration Mixture", + "@language": "en" + }, + { + "@value": "إعادة التهوية الاختلاط", + "@language": "ar" + }, + { + "@value": "Re-hydration Mixture", + "@language": "ku" + }, + { + "@value": "Rehidratación Mezcla", + "@language": "es" + }, + { + "@value": "Reidratazione Miscela", + "@language": "it" + }, + { + "@value": "Rehydrierung Mischung", + "@language": "de" + }, + { + "@value": "Re-hydration Mixture", + "@language": "sw" + }, + { + "@value": "Re-hidratação Mistura", + "@language": "pt" + }, + { + "@value": "Re-hydration Mixture", + "@language": "oc" + }, + { + "@value": "Ре-гидация Смесь", + "@language": "ru" + }, + { + "@value": "Re-hydration Mixture", + "@language": "cy" + }, + { + "@value": "再水分補給 ミックスチャー", + "@language": "ja" + }, + { + "@value": "Ath-hiodráitiú Meascthóir", + "@language": "ga" + }, + { + "@value": "पुनर्जलीकरण मिश्रण", + "@language": "hi" + }, + { + "@value": "重新计算 固定", + "@language": "zh" + }, + { + "@value": "Réhydratation Mixture", + "@language": "fr" + }, + { + "@value": "Re-hydration Mixture", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#fluids", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://medical/data/medicalTypes.rdf#ear-plugs", + "rdfs:label": [ + { + "@value": "Ear Plugs", + "@language": "en" + }, + { + "@value": "Ear Plugs", + "@language": "ar" + }, + { + "@value": "Ear Plugs", + "@language": "ku" + }, + { + "@value": "Plugs de oído", + "@language": "es" + }, + { + "@value": "Spine per orecchie", + "@language": "it" + }, + { + "@value": "Ohrstecker", + "@language": "de" + }, + { + "@value": "Ear Plugs", + "@language": "sw" + }, + { + "@value": "Plugs de ouvido", + "@language": "pt" + }, + { + "@value": "Ear Plugs", + "@language": "oc" + }, + { + "@value": "Ear Plugs", + "@language": "ru" + }, + { + "@value": "Ear Plugs", + "@language": "cy" + }, + { + "@value": "イヤープラグ", + "@language": "ja" + }, + { + "@value": "Seirbhís do Chustaiméirí", + "@language": "ga" + }, + { + "@value": "कान प्लग", + "@language": "hi" + }, + { + "@value": "Ear Plugs", + "@language": "zh" + }, + { + "@value": "Plugs d'oreille", + "@language": "fr" + }, + { + "@value": "Ear Plugs", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#body-protection", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://medical/data/medicalTypes.rdf#cpr-mask", + "rdfs:label": [ + { + "@value": "CPR Mask", + "@language": "en" + }, + { + "@value": "CPR Mask", + "@language": "ar" + }, + { + "@value": "CPR Mask", + "@language": "ku" + }, + { + "@value": "CPR Máscara", + "@language": "es" + }, + { + "@value": "RCP Maschera", + "@language": "it" + }, + { + "@value": "CPR Maske", + "@language": "de" + }, + { + "@value": "CPR Mask", + "@language": "sw" + }, + { + "@value": "RCP Máscara", + "@language": "pt" + }, + { + "@value": "CPR Mask", + "@language": "oc" + }, + { + "@value": "КПР Маска", + "@language": "ru" + }, + { + "@value": "CPR Mask", + "@language": "cy" + }, + { + "@value": "CPRの特長 マスク", + "@language": "ja" + }, + { + "@value": "CPR An chuid is mó", + "@language": "ga" + }, + { + "@value": "सीपीआर मास्क", + "@language": "hi" + }, + { + "@value": "评 注 Mask", + "@language": "zh" + }, + { + "@value": "CPR Mask", + "@language": "fr" + }, + { + "@value": "CPR Mask", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://medical/data/medicalTypes.rdf#mask", + "@type": "dfc-p:ProductType" + } + ] +} diff --git a/ontology/toolTypes.json b/ontology/toolTypes.json new file mode 100644 index 000000000..94d17e455 --- /dev/null +++ b/ontology/toolTypes.json @@ -0,0 +1,12914 @@ +{ + "@context": { + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "dfc-b": "http://static.datafoodconsortium.org/ontologies/DFC_BusinessOntology.owl#", + "dfc-p": "http://static.datafoodconsortium.org/ontologies/DFC_ProductOntology.owl#", + "dfc-t": "http://static.datafoodconsortium.org/ontologies/DFC_TechnicalOntology.owl#", + "dfc-u": "http://static.datafoodconsortium.org/data/units.rdf#", + "dfc-p:specialize": { + "@type": "@id" + } + }, + "@graph": [ + { + "@id": "https://tools/data/toolTypes.rdf#garden-trowel", + "rdfs:label": [ + { + "@value": "Truelle de jardin", + "@language": "fr" + }, + { + "@value": "Llana de jardín", + "@language": "es" + }, + { + "@value": "Gartenkanel", + "@language": "de" + }, + { + "@value": "Garden Trowel", + "@language": "en" + }, + { + "@value": "غاردين تروميل", + "@language": "ar" + }, + { + "@value": "Garden Trowel", + "@language": "ku" + }, + { + "@value": "Trowel da giardino", + "@language": "it" + }, + { + "@value": "Garden Trowel", + "@language": "sw" + }, + { + "@value": "Tropa de jardim", + "@language": "pt" + }, + { + "@value": "Garden Trowel", + "@language": "oc" + }, + { + "@value": "Садовая дорожка", + "@language": "ru" + }, + { + "@value": "Garden Trowel", + "@language": "cy" + }, + { + "@value": "ガーデン トロウェル", + "@language": "ja" + }, + { + "@value": "cliceáil grianghraf a mhéadú", + "@language": "ga" + }, + { + "@value": "गार्डन Trowel", + "@language": "hi" + }, + { + "@value": "Garden Trowel", + "@language": "zh" + }, + { + "@value": "Garden Trowel", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#watering-can", + "rdfs:label": [ + { + "@value": "Arrosoir", + "@language": "fr" + }, + { + "@value": "Regadera", + "@language": "es" + }, + { + "@value": "Gießkanne", + "@language": "de" + }, + { + "@value": "Watering Can", + "@language": "en" + }, + { + "@value": "Watering Can", + "@language": "ar" + }, + { + "@value": "Watering Can", + "@language": "ku" + }, + { + "@value": "Canna da irrigazione", + "@language": "it" + }, + { + "@value": "Watering Can", + "@language": "sw" + }, + { + "@value": "Pode de rega", + "@language": "pt" + }, + { + "@value": "Watering Can", + "@language": "oc" + }, + { + "@value": "Вода может", + "@language": "ru" + }, + { + "@value": "Watering Can", + "@language": "cy" + }, + { + "@value": "水をまくことはできます", + "@language": "ja" + }, + { + "@value": "An féidir Uisce", + "@language": "ga" + }, + { + "@value": "पानी कैन", + "@language": "hi" + }, + { + "@value": "水果", + "@language": "zh" + }, + { + "@value": "Watering Can", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#auger", + "rdfs:label": [ + { + "@value": "Tailleuse", + "@language": "fr" + }, + { + "@value": "Barrena", + "@language": "es" + }, + { + "@value": "Schnecke", + "@language": "de" + }, + { + "@value": "Auger", + "@language": "en" + }, + { + "@value": "Auger", + "@language": "ar" + }, + { + "@value": "Auger", + "@language": "ku" + }, + { + "@value": "Auger", + "@language": "it" + }, + { + "@value": "Auger", + "@language": "sw" + }, + { + "@value": "Auger", + "@language": "pt" + }, + { + "@value": "Auger", + "@language": "oc" + }, + { + "@value": "Авгэр", + "@language": "ru" + }, + { + "@value": "Auger", + "@language": "cy" + }, + { + "@value": "オーガー", + "@language": "ja" + }, + { + "@value": "An t-eagrán is déanaí", + "@language": "ga" + }, + { + "@value": "ऑगर", + "@language": "hi" + }, + { + "@value": "Auger", + "@language": "zh" + }, + { + "@value": "Auger", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#backpack-sprayer", + "rdfs:label": [ + { + "@value": "Pulvérisateur à dos", + "@language": "fr" + }, + { + "@value": "Pulverizador de mochila", + "@language": "es" + }, + { + "@value": "Rucksack-Sprayer", + "@language": "de" + }, + { + "@value": "Backpack Sprayer", + "@language": "en" + }, + { + "@value": "حقيبة ظهر", + "@language": "ar" + }, + { + "@value": "Backpack Sprayer", + "@language": "ku" + }, + { + "@value": "Spray per zaini", + "@language": "it" + }, + { + "@value": "Backpack Sprayer", + "@language": "sw" + }, + { + "@value": "Pulverizador de mochila", + "@language": "pt" + }, + { + "@value": "Backpack Sprayer", + "@language": "oc" + }, + { + "@value": "Рюкзак Sprayer", + "@language": "ru" + }, + { + "@value": "Backpack Sprayer", + "@language": "cy" + }, + { + "@value": "バックパックスプレーヤー", + "@language": "ja" + }, + { + "@value": "Spraeire Backpack", + "@language": "ga" + }, + { + "@value": "बैकपैक स्प्रेयर", + "@language": "hi" + }, + { + "@value": "Backpack喷雾器", + "@language": "zh" + }, + { + "@value": "Backpack Sprayer", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#sprayer", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#border-spade", + "rdfs:label": [ + { + "@value": "Pelle frontalière", + "@language": "fr" + }, + { + "@value": "Pala fronteriza", + "@language": "es" + }, + { + "@value": "Border Spade", + "@language": "de" + }, + { + "@value": "Border Spade", + "@language": "en" + }, + { + "@value": "الحدود", + "@language": "ar" + }, + { + "@value": "Border Spade", + "@language": "ku" + }, + { + "@value": "Spade di confine", + "@language": "it" + }, + { + "@value": "Border Spade", + "@language": "sw" + }, + { + "@value": "Spade de fronteira", + "@language": "pt" + }, + { + "@value": "Border Spade", + "@language": "oc" + }, + { + "@value": "Пограничный спад", + "@language": "ru" + }, + { + "@value": "Border Spade", + "@language": "cy" + }, + { + "@value": "ボーダースパード", + "@language": "ja" + }, + { + "@value": "Teorainn Spa", + "@language": "ga" + }, + { + "@value": "बॉर्डर स्पैड", + "@language": "hi" + }, + { + "@value": "边界项目", + "@language": "zh" + }, + { + "@value": "Border Spade", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#bow-rake", + "rdfs:label": [ + { + "@value": "Râteau d'arc", + "@language": "fr" + }, + { + "@value": "Rake de arco", + "@language": "es" + }, + { + "@value": "Bug Rake", + "@language": "de" + }, + { + "@value": "Bow Rake", + "@language": "en" + }, + { + "@value": "Bow Rake", + "@language": "ar" + }, + { + "@value": "Bow Rake", + "@language": "ku" + }, + { + "@value": "Fiocco di prua", + "@language": "it" + }, + { + "@value": "Bow Rake", + "@language": "sw" + }, + { + "@value": "Bow Rake", + "@language": "pt" + }, + { + "@value": "Bow Rake", + "@language": "oc" + }, + { + "@value": "Лук Рейк", + "@language": "ru" + }, + { + "@value": "Bow Rake", + "@language": "cy" + }, + { + "@value": "ボウレーク", + "@language": "ja" + }, + { + "@value": "Uaireadóirí macasamhail", + "@language": "ga" + }, + { + "@value": "बो रैक", + "@language": "hi" + }, + { + "@value": "Bow Rake", + "@language": "zh" + }, + { + "@value": "Bow Rake", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#broadfork", + "rdfs:label": [ + { + "@value": "Larges", + "@language": "fr" + }, + { + "@value": "Bloadfork", + "@language": "es" + }, + { + "@value": "Broadfork", + "@language": "de" + }, + { + "@value": "Broadfork", + "@language": "en" + }, + { + "@value": "Broadfork", + "@language": "ar" + }, + { + "@value": "Broadfork", + "@language": "ku" + }, + { + "@value": "Broadfork", + "@language": "it" + }, + { + "@value": "Broadfork", + "@language": "sw" + }, + { + "@value": "Abertura", + "@language": "pt" + }, + { + "@value": "Broadfork", + "@language": "oc" + }, + { + "@value": "Бродфорк", + "@language": "ru" + }, + { + "@value": "Broadfork", + "@language": "cy" + }, + { + "@value": "ブロードフォーク", + "@language": "ja" + }, + { + "@value": "An t-eolas is déanaí", + "@language": "ga" + }, + { + "@value": "ब्रॉडफोर्क", + "@language": "hi" + }, + { + "@value": "B. 路包", + "@language": "zh" + }, + { + "@value": "Broadfork", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#budding-knife", + "rdfs:label": [ + { + "@value": "Couteau en herbe", + "@language": "fr" + }, + { + "@value": "Cuchillo en ciernes", + "@language": "es" + }, + { + "@value": "Knospemesser", + "@language": "de" + }, + { + "@value": "Budding Knife", + "@language": "en" + }, + { + "@value": "بودنغ كنيف", + "@language": "ar" + }, + { + "@value": "Budding Knife", + "@language": "ku" + }, + { + "@value": "Coltello in maglia", + "@language": "it" + }, + { + "@value": "Budding Knife", + "@language": "sw" + }, + { + "@value": "Faca de amor", + "@language": "pt" + }, + { + "@value": "Budding Knife", + "@language": "oc" + }, + { + "@value": "Будинг Нож", + "@language": "ru" + }, + { + "@value": "Budding Knife", + "@language": "cy" + }, + { + "@value": "ブッシュナイフ", + "@language": "ja" + }, + { + "@value": "Clúdaigh bainise", + "@language": "ga" + }, + { + "@value": "बडिंग चाकू", + "@language": "hi" + }, + { + "@value": "D. 布设Knife", + "@language": "zh" + }, + { + "@value": "Budding Knife", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#cutting-tool", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#bulb-planter", + "rdfs:label": [ + { + "@value": "Planteur d'ampoule", + "@language": "fr" + }, + { + "@value": "Plantador de bombilla", + "@language": "es" + }, + { + "@value": "Birnenpflanzer", + "@language": "de" + }, + { + "@value": "Bulb Planter", + "@language": "en" + }, + { + "@value": "Bulb Planter", + "@language": "ar" + }, + { + "@value": "Bulb Planter", + "@language": "ku" + }, + { + "@value": "Piantatrice a bulbo", + "@language": "it" + }, + { + "@value": "Bulb Planter", + "@language": "sw" + }, + { + "@value": "Plantador de massa", + "@language": "pt" + }, + { + "@value": "Bulb Planter", + "@language": "oc" + }, + { + "@value": "Лампа Planter", + "@language": "ru" + }, + { + "@value": "Bulb Planter", + "@language": "cy" + }, + { + "@value": "電球プランター", + "@language": "ja" + }, + { + "@value": "duille dath: glas", + "@language": "ga" + }, + { + "@value": "बल्ब प्लेंटर", + "@language": "hi" + }, + { + "@value": "Bulb Planter", + "@language": "zh" + }, + { + "@value": "Bulb Planter", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#compost-bin", + "rdfs:label": [ + { + "@value": "Bac à compost", + "@language": "fr" + }, + { + "@value": "Cesto de basura", + "@language": "es" + }, + { + "@value": "Kompost Tonne", + "@language": "de" + }, + { + "@value": "Compost Bin", + "@language": "en" + }, + { + "@value": "Compost Bin", + "@language": "ar" + }, + { + "@value": "Compost Bin", + "@language": "ku" + }, + { + "@value": "Cestino di Compost", + "@language": "it" + }, + { + "@value": "Compost Bin", + "@language": "sw" + }, + { + "@value": "Binário de compostagem", + "@language": "pt" + }, + { + "@value": "Compost Bin", + "@language": "oc" + }, + { + "@value": "Компост Бин", + "@language": "ru" + }, + { + "@value": "Compost Bin", + "@language": "cy" + }, + { + "@value": "コンポストビン", + "@language": "ja" + }, + { + "@value": "Déan teagmháil Linn", + "@language": "ga" + }, + { + "@value": "कंपोस्ट बिन", + "@language": "hi" + }, + { + "@value": "议 程", + "@language": "zh" + }, + { + "@value": "Compost Bin", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#container", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#compost-fork", + "rdfs:label": [ + { + "@value": "Fourchette de compost", + "@language": "fr" + }, + { + "@value": "Tenedor de compost", + "@language": "es" + }, + { + "@value": "Kompostgabel", + "@language": "de" + }, + { + "@value": "Compost Fork", + "@language": "en" + }, + { + "@value": "الشوكة", + "@language": "ar" + }, + { + "@value": "Compost Fork", + "@language": "ku" + }, + { + "@value": "Forcella Compost", + "@language": "it" + }, + { + "@value": "Compost Fork", + "@language": "sw" + }, + { + "@value": "Fork de composto", + "@language": "pt" + }, + { + "@value": "Compost Fork", + "@language": "oc" + }, + { + "@value": "Компост Форк", + "@language": "ru" + }, + { + "@value": "Compost Fork", + "@language": "cy" + }, + { + "@value": "コンポストフォーク", + "@language": "ja" + }, + { + "@value": "Déan Teagmháil Linn", + "@language": "ga" + }, + { + "@value": "कंपोस्ट फोर्क", + "@language": "hi" + }, + { + "@value": "科 福克尔", + "@language": "zh" + }, + { + "@value": "Compost Fork", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#core-aerator", + "rdfs:label": [ + { + "@value": "Aérateur de noyau", + "@language": "fr" + }, + { + "@value": "Aerodinador central", + "@language": "es" + }, + { + "@value": "Kernbelüterer", + "@language": "de" + }, + { + "@value": "Core Aerator", + "@language": "en" + }, + { + "@value": "المحرر الأساسي", + "@language": "ar" + }, + { + "@value": "Core Aerator", + "@language": "ku" + }, + { + "@value": "Core Aerator", + "@language": "it" + }, + { + "@value": "Core Aerator", + "@language": "sw" + }, + { + "@value": "Aerador de núcleo", + "@language": "pt" + }, + { + "@value": "Core Aerator", + "@language": "oc" + }, + { + "@value": "Ядро Aerator", + "@language": "ru" + }, + { + "@value": "Core Aerator", + "@language": "cy" + }, + { + "@value": "コア・アエレータ", + "@language": "ja" + }, + { + "@value": "Aerárthaí Croí", + "@language": "ga" + }, + { + "@value": "कोर Aerator", + "@language": "hi" + }, + { + "@value": "核心小组", + "@language": "zh" + }, + { + "@value": "Core Aerator", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#drum-aerators", + "rdfs:label": [ + { + "@value": "Aérateurs de tambour", + "@language": "fr" + }, + { + "@value": "Aeradores de tambores", + "@language": "es" + }, + { + "@value": "Trommelbelüfter", + "@language": "de" + }, + { + "@value": "Drum Aerators", + "@language": "en" + }, + { + "@value": "Drum Aerators", + "@language": "ar" + }, + { + "@value": "Drum Aerators", + "@language": "ku" + }, + { + "@value": "Aeratori del tamburo", + "@language": "it" + }, + { + "@value": "Drum Aerators", + "@language": "sw" + }, + { + "@value": "Aeradores de tambor", + "@language": "pt" + }, + { + "@value": "Drum Aerators", + "@language": "oc" + }, + { + "@value": "Барабан Aerators", + "@language": "ru" + }, + { + "@value": "Drum Aerators", + "@language": "cy" + }, + { + "@value": "ドラム・アレイタ", + "@language": "ja" + }, + { + "@value": "Amharc ar gach eolas", + "@language": "ga" + }, + { + "@value": "ड्रम एरेटर", + "@language": "hi" + }, + { + "@value": "Drum Aer", + "@language": "zh" + }, + { + "@value": "Drum Aerators", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#edging-shears", + "rdfs:label": [ + { + "@value": "Édition de cisailles", + "@language": "fr" + }, + { + "@value": "Cizallas de bordes", + "@language": "es" + }, + { + "@value": "Randschere", + "@language": "de" + }, + { + "@value": "Edging Shears", + "@language": "en" + }, + { + "@value": "الماشية", + "@language": "ar" + }, + { + "@value": "Edging Shears", + "@language": "ku" + }, + { + "@value": "Equitazione di Shears", + "@language": "it" + }, + { + "@value": "Edging Shears", + "@language": "sw" + }, + { + "@value": "Edging Shears", + "@language": "pt" + }, + { + "@value": "Edging Shears", + "@language": "oc" + }, + { + "@value": "Судьба шейар", + "@language": "ru" + }, + { + "@value": "Edging Shears", + "@language": "cy" + }, + { + "@value": "ヒアリング", + "@language": "ja" + }, + { + "@value": "Seirbhís do Chustaiméirí", + "@language": "ga" + }, + { + "@value": "एजिंग शेर", + "@language": "hi" + }, + { + "@value": "D. 培育女", + "@language": "zh" + }, + { + "@value": "Edging Shears", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#electric-edger", + "rdfs:label": [ + { + "@value": "Edger électrique", + "@language": "fr" + }, + { + "@value": "Borde eléctrico", + "@language": "es" + }, + { + "@value": "Elektrischer Edger", + "@language": "de" + }, + { + "@value": "Electric Edger", + "@language": "en" + }, + { + "@value": "Electric Edger", + "@language": "ar" + }, + { + "@value": "Electric Edger", + "@language": "ku" + }, + { + "@value": "Bordo elettrico", + "@language": "it" + }, + { + "@value": "Electric Edger", + "@language": "sw" + }, + { + "@value": "Borda elétrica", + "@language": "pt" + }, + { + "@value": "Electric Edger", + "@language": "oc" + }, + { + "@value": "Электрический Edger", + "@language": "ru" + }, + { + "@value": "Electric Edger", + "@language": "cy" + }, + { + "@value": "電気エッジ", + "@language": "ja" + }, + { + "@value": "Leictreach Edge", + "@language": "ga" + }, + { + "@value": "इलेक्ट्रिक एडगर", + "@language": "hi" + }, + { + "@value": "电热", + "@language": "zh" + }, + { + "@value": "Electric Edger", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#flat-rake", + "rdfs:label": [ + { + "@value": "Râteau plat", + "@language": "fr" + }, + { + "@value": "Rastrillo plano", + "@language": "es" + }, + { + "@value": "Flacher Rechen", + "@language": "de" + }, + { + "@value": "Flat Rake", + "@language": "en" + }, + { + "@value": "Rake", + "@language": "ar" + }, + { + "@value": "Flat Rake", + "@language": "ku" + }, + { + "@value": "Rake piatto", + "@language": "it" + }, + { + "@value": "Flat Rake", + "@language": "sw" + }, + { + "@value": "Rake plano", + "@language": "pt" + }, + { + "@value": "Flat Rake", + "@language": "oc" + }, + { + "@value": "Плоский рейк", + "@language": "ru" + }, + { + "@value": "Flat Rake", + "@language": "cy" + }, + { + "@value": "フラットレイク", + "@language": "ja" + }, + { + "@value": "Uisce agus Séarachas", + "@language": "ga" + }, + { + "@value": "फ्लैट रेक", + "@language": "hi" + }, + { + "@value": "Flat Rake", + "@language": "zh" + }, + { + "@value": "Flat Rake", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#garden-fork", + "rdfs:label": [ + { + "@value": "Fourche à bêcher", + "@language": "fr" + }, + { + "@value": "Tenedor de jardín", + "@language": "es" + }, + { + "@value": "Gartengabel", + "@language": "de" + }, + { + "@value": "Garden Fork", + "@language": "en" + }, + { + "@value": "فورد", + "@language": "ar" + }, + { + "@value": "Garden Fork", + "@language": "ku" + }, + { + "@value": "Forcella da giardino", + "@language": "it" + }, + { + "@value": "Garden Fork", + "@language": "sw" + }, + { + "@value": "Garrafa de jardim", + "@language": "pt" + }, + { + "@value": "Garden Fork", + "@language": "oc" + }, + { + "@value": "Сад Fork", + "@language": "ru" + }, + { + "@value": "Garden Fork", + "@language": "cy" + }, + { + "@value": "ガーデンフォーク", + "@language": "ja" + }, + { + "@value": "Uirlisí ilchuspóireacha", + "@language": "ga" + }, + { + "@value": "गार्डन फोर्क", + "@language": "hi" + }, + { + "@value": "Garden Fork", + "@language": "zh" + }, + { + "@value": "Garden Fork", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#garden-hoe", + "rdfs:label": [ + { + "@value": "Jardin houe", + "@language": "fr" + }, + { + "@value": "Azada del jardín", + "@language": "es" + }, + { + "@value": "Gartenhacke", + "@language": "de" + }, + { + "@value": "Garden Hoe", + "@language": "en" + }, + { + "@value": "Garden Hoe", + "@language": "ar" + }, + { + "@value": "Garden Hoe", + "@language": "ku" + }, + { + "@value": "Hoe del giardino", + "@language": "it" + }, + { + "@value": "Garden Hoe", + "@language": "sw" + }, + { + "@value": "Hoe do jardim", + "@language": "pt" + }, + { + "@value": "Garden Hoe", + "@language": "oc" + }, + { + "@value": "Сад Hoe", + "@language": "ru" + }, + { + "@value": "Garden Hoe", + "@language": "cy" + }, + { + "@value": "ガーデン ホー", + "@language": "ja" + }, + { + "@value": "taiseachas aeir: fliuch", + "@language": "ga" + }, + { + "@value": "गार्डन हो", + "@language": "hi" + }, + { + "@value": "Garden Hoe", + "@language": "zh" + }, + { + "@value": "Garden Hoe", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#garden-shovel", + "rdfs:label": [ + { + "@value": "Pelle de jardin", + "@language": "fr" + }, + { + "@value": "Pala de jardín", + "@language": "es" + }, + { + "@value": "Gartenschaufel", + "@language": "de" + }, + { + "@value": "Garden Shovel", + "@language": "en" + }, + { + "@value": "Garden Shovel", + "@language": "ar" + }, + { + "@value": "Garden Shovel", + "@language": "ku" + }, + { + "@value": "Scivolo da giardino", + "@language": "it" + }, + { + "@value": "Garden Shovel", + "@language": "sw" + }, + { + "@value": "Tiro do jardim", + "@language": "pt" + }, + { + "@value": "Garden Shovel", + "@language": "oc" + }, + { + "@value": "Сад Шовель", + "@language": "ru" + }, + { + "@value": "Garden Shovel", + "@language": "cy" + }, + { + "@value": "ガーデンショベル", + "@language": "ja" + }, + { + "@value": "Gairdín Shovel", + "@language": "ga" + }, + { + "@value": "गार्डन फावड़ा", + "@language": "hi" + }, + { + "@value": "Garden Shovel", + "@language": "zh" + }, + { + "@value": "Garden Shovel", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#shovel", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#gas-powered-lawn-edger", + "rdfs:label": [ + { + "@value": "Edger à gaz à gaz", + "@language": "fr" + }, + { + "@value": "Edger de césped accionado en gas", + "@language": "es" + }, + { + "@value": "Gasbetriebener Rasenfernrand", + "@language": "de" + }, + { + "@value": "Gas Powered Lawn Edger", + "@language": "en" + }, + { + "@value": "Gas Powered Lawn Edger", + "@language": "ar" + }, + { + "@value": "Gas Powered Lawn Edger", + "@language": "ku" + }, + { + "@value": "Guarnizione da giardino alimentato a gas", + "@language": "it" + }, + { + "@value": "Gas Powered Lawn Edger", + "@language": "sw" + }, + { + "@value": "Borda de gramado alimentado a gás", + "@language": "pt" + }, + { + "@value": "Gas Powered Lawn Edger", + "@language": "oc" + }, + { + "@value": "Газ Питаемый газон Edger", + "@language": "ru" + }, + { + "@value": "Gas Powered Lawn Edger", + "@language": "cy" + }, + { + "@value": "ガス駆動の芝生エッジ", + "@language": "ja" + }, + { + "@value": "Gás Powered Lawn Edger", + "@language": "ga" + }, + { + "@value": "गैस पावर्ड लॉन एडगर", + "@language": "hi" + }, + { + "@value": "A. 强权", + "@language": "zh" + }, + { + "@value": "Gas Powered Lawn Edger", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#gardening-gloves", + "rdfs:label": [ + { + "@value": "Gants de jardinage", + "@language": "fr" + }, + { + "@value": "Guantes de jardineria", + "@language": "es" + }, + { + "@value": "Gartenhandschuhe", + "@language": "de" + }, + { + "@value": "Gardening Gloves", + "@language": "en" + }, + { + "@value": "قسائم القمامة", + "@language": "ar" + }, + { + "@value": "Gardening Gloves", + "@language": "ku" + }, + { + "@value": "Guanti da giardino", + "@language": "it" + }, + { + "@value": "Gardening Gloves", + "@language": "sw" + }, + { + "@value": "Luvas de jardinagem", + "@language": "pt" + }, + { + "@value": "Gardening Gloves", + "@language": "oc" + }, + { + "@value": "Садовые перчатки", + "@language": "ru" + }, + { + "@value": "Gardening Gloves", + "@language": "cy" + }, + { + "@value": "ガーデニンググローブ", + "@language": "ja" + }, + { + "@value": "Lámhainní Gairdín", + "@language": "ga" + }, + { + "@value": "बागवानी दस्ताने", + "@language": "hi" + }, + { + "@value": "Gdendenes", + "@language": "zh" + }, + { + "@value": "Gardening Gloves", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#hand-cultivator", + "rdfs:label": [ + { + "@value": "Cultivateur de mains", + "@language": "fr" + }, + { + "@value": "Cultivador de manos", + "@language": "es" + }, + { + "@value": "Handgrubber", + "@language": "de" + }, + { + "@value": "Hand Cultivator", + "@language": "en" + }, + { + "@value": "التعبئة اليدوية", + "@language": "ar" + }, + { + "@value": "Hand Cultivator", + "@language": "ku" + }, + { + "@value": "Coltivatore a mano", + "@language": "it" + }, + { + "@value": "Hand Cultivator", + "@language": "sw" + }, + { + "@value": "Cultivador de mão", + "@language": "pt" + }, + { + "@value": "Hand Cultivator", + "@language": "oc" + }, + { + "@value": "Культиватор рук", + "@language": "ru" + }, + { + "@value": "Hand Cultivator", + "@language": "cy" + }, + { + "@value": "ハンドキュレーター", + "@language": "ja" + }, + { + "@value": "Cultator Hand", + "@language": "ga" + }, + { + "@value": "हाथ कल्टीवेटर", + "@language": "hi" + }, + { + "@value": "汉德·克鲁夫", + "@language": "zh" + }, + { + "@value": "Hand Cultivator", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#handheld-sprayer", + "rdfs:label": [ + { + "@value": "Pulvérisateur de poche", + "@language": "fr" + }, + { + "@value": "Pulverizador de mano", + "@language": "es" + }, + { + "@value": "Handheld-Sprühgerät", + "@language": "de" + }, + { + "@value": "Handheld Sprayer", + "@language": "en" + }, + { + "@value": "Sprayer Handheld", + "@language": "ar" + }, + { + "@value": "Handheld Sprayer", + "@language": "ku" + }, + { + "@value": "Spruzzatore portatile", + "@language": "it" + }, + { + "@value": "Handheld Sprayer", + "@language": "sw" + }, + { + "@value": "Pulverizador portátil", + "@language": "pt" + }, + { + "@value": "Handheld Sprayer", + "@language": "oc" + }, + { + "@value": "Handheld опрыскиватель", + "@language": "ru" + }, + { + "@value": "Handheld Sprayer", + "@language": "cy" + }, + { + "@value": "手持ち型のスプレーヤー", + "@language": "ja" + }, + { + "@value": "Spraeire láimhe", + "@language": "ga" + }, + { + "@value": "हाथ में स्प्रेयर", + "@language": "hi" + }, + { + "@value": "汉德·佩雷", + "@language": "zh" + }, + { + "@value": "Handheld Sprayer", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#hand-seeder", + "rdfs:label": [ + { + "@value": "Semoir à la main", + "@language": "fr" + }, + { + "@value": "Sembradora de mano", + "@language": "es" + }, + { + "@value": "Handsämaschine", + "@language": "de" + }, + { + "@value": "Hand Seeder", + "@language": "en" + }, + { + "@value": "Hand Seeder", + "@language": "ar" + }, + { + "@value": "Hand Seeder", + "@language": "ku" + }, + { + "@value": "Separatore a mano", + "@language": "it" + }, + { + "@value": "Hand Seeder", + "@language": "sw" + }, + { + "@value": "Máquina de montagem automática", + "@language": "pt" + }, + { + "@value": "Hand Seeder", + "@language": "oc" + }, + { + "@value": "Сеялка для рук", + "@language": "ru" + }, + { + "@value": "Hand Seeder", + "@language": "cy" + }, + { + "@value": "手のシーダー", + "@language": "ja" + }, + { + "@value": "Láimh", + "@language": "ga" + }, + { + "@value": "हाथ सीडर", + "@language": "hi" + }, + { + "@value": "D. 汉语", + "@language": "zh" + }, + { + "@value": "Hand Seeder", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#hedge-shears", + "rdfs:label": [ + { + "@value": "Cisailles de haie", + "@language": "fr" + }, + { + "@value": "Cizallas de seto", + "@language": "es" + }, + { + "@value": "Heckenschere", + "@language": "de" + }, + { + "@value": "Hedge Shears", + "@language": "en" + }, + { + "@value": "Hedge Shears", + "@language": "ar" + }, + { + "@value": "Hedge Shears", + "@language": "ku" + }, + { + "@value": "Hedge Shears", + "@language": "it" + }, + { + "@value": "Hedge Shears", + "@language": "sw" + }, + { + "@value": "Tesoura de borda", + "@language": "pt" + }, + { + "@value": "Hedge Shears", + "@language": "oc" + }, + { + "@value": "Хедж Шарс", + "@language": "ru" + }, + { + "@value": "Hedge Shears", + "@language": "cy" + }, + { + "@value": "ヘッジシャー", + "@language": "ja" + }, + { + "@value": "riachtanais uisce: measartha", + "@language": "ga" + }, + { + "@value": "हेज शीयर", + "@language": "hi" + }, + { + "@value": "Hedge Shears", + "@language": "zh" + }, + { + "@value": "Hedge Shears", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#hoe", + "rdfs:label": [ + { + "@value": "Houe", + "@language": "fr" + }, + { + "@value": "Azada", + "@language": "es" + }, + { + "@value": "Hacke", + "@language": "de" + }, + { + "@value": "Hoe", + "@language": "en" + }, + { + "@value": "Hoe", + "@language": "ar" + }, + { + "@value": "Hoe", + "@language": "ku" + }, + { + "@value": "Hoe", + "@language": "it" + }, + { + "@value": "Hoe", + "@language": "sw" + }, + { + "@value": "Hoe", + "@language": "pt" + }, + { + "@value": "Hoe", + "@language": "oc" + }, + { + "@value": "Хой", + "@language": "ru" + }, + { + "@value": "Hoe", + "@language": "cy" + }, + { + "@value": "ホエ", + "@language": "ja" + }, + { + "@value": "taiseachas aeir: fliuch", + "@language": "ga" + }, + { + "@value": "हो", + "@language": "hi" + }, + { + "@value": "霍 埃", + "@language": "zh" + }, + { + "@value": "Hoe", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#kneeler", + "rdfs:label": [ + { + "@value": "Agenouilloir", + "@language": "fr" + }, + { + "@value": "Arrodillador", + "@language": "es" + }, + { + "@value": "Keeler", + "@language": "de" + }, + { + "@value": "Kneeler", + "@language": "en" + }, + { + "@value": "Kneeler", + "@language": "ar" + }, + { + "@value": "Kneeler", + "@language": "ku" + }, + { + "@value": "Ginocchiera", + "@language": "it" + }, + { + "@value": "Kneeler", + "@language": "sw" + }, + { + "@value": "Amassadeira", + "@language": "pt" + }, + { + "@value": "Kneeler", + "@language": "oc" + }, + { + "@value": "Кнелер", + "@language": "ru" + }, + { + "@value": "Kneeler", + "@language": "cy" + }, + { + "@value": "クネラー", + "@language": "ja" + }, + { + "@value": "An tSraith Shinsearach", + "@language": "ga" + }, + { + "@value": "घुटने", + "@language": "hi" + }, + { + "@value": "Kneler", + "@language": "zh" + }, + { + "@value": "Kneeler", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#lawn-mower", + "rdfs:label": [ + { + "@value": "Tondeuse à gazon", + "@language": "fr" + }, + { + "@value": "Cortacésped", + "@language": "es" + }, + { + "@value": "Rasenmäher", + "@language": "de" + }, + { + "@value": "Lawn Mower", + "@language": "en" + }, + { + "@value": "لون مور", + "@language": "ar" + }, + { + "@value": "Lawn Mower", + "@language": "ku" + }, + { + "@value": "Mower tosaerba", + "@language": "it" + }, + { + "@value": "Lawn Mower", + "@language": "sw" + }, + { + "@value": "Cortador de relva", + "@language": "pt" + }, + { + "@value": "Lawn Mower", + "@language": "oc" + }, + { + "@value": "Лоуна Mower", + "@language": "ru" + }, + { + "@value": "Lawn Mower", + "@language": "cy" + }, + { + "@value": "芝刈り機", + "@language": "ja" + }, + { + "@value": "Seirbhís do Chustaiméirí", + "@language": "ga" + }, + { + "@value": "लॉन मोवर", + "@language": "hi" + }, + { + "@value": "法 卫", + "@language": "zh" + }, + { + "@value": "Lawn Mower", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#leaf-blower", + "rdfs:label": [ + { + "@value": "Souffleur de feuilles", + "@language": "fr" + }, + { + "@value": "Soplador de hojas", + "@language": "es" + }, + { + "@value": "Laubbläser", + "@language": "de" + }, + { + "@value": "Leaf Blower", + "@language": "en" + }, + { + "@value": "Leaf Blower", + "@language": "ar" + }, + { + "@value": "Leaf Blower", + "@language": "ku" + }, + { + "@value": "Soffietto a foglia", + "@language": "it" + }, + { + "@value": "Leaf Blower", + "@language": "sw" + }, + { + "@value": "Ventilador de folhas", + "@language": "pt" + }, + { + "@value": "Leaf Blower", + "@language": "oc" + }, + { + "@value": "Глухое Blower", + "@language": "ru" + }, + { + "@value": "Leaf Blower", + "@language": "cy" + }, + { + "@value": "葉の送風機", + "@language": "ja" + }, + { + "@value": "bláthanna cumhra: cumhráin", + "@language": "ga" + }, + { + "@value": "पत्ता ब्लोअर", + "@language": "hi" + }, + { + "@value": "Leaf Blower", + "@language": "zh" + }, + { + "@value": "Leaf Blower", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#leaf-rake", + "rdfs:label": [ + { + "@value": "Râteau de feuilles", + "@language": "fr" + }, + { + "@value": "Rastrillo de hojas", + "@language": "es" + }, + { + "@value": "Blatt-Rechen", + "@language": "de" + }, + { + "@value": "Leaf Rake", + "@language": "en" + }, + { + "@value": "ليف راك", + "@language": "ar" + }, + { + "@value": "Leaf Rake", + "@language": "ku" + }, + { + "@value": "Leaf Rake", + "@language": "it" + }, + { + "@value": "Leaf Rake", + "@language": "sw" + }, + { + "@value": "Rake de folha", + "@language": "pt" + }, + { + "@value": "Leaf Rake", + "@language": "oc" + }, + { + "@value": "Леф Рак", + "@language": "ru" + }, + { + "@value": "Leaf Rake", + "@language": "cy" + }, + { + "@value": "リーフレイク", + "@language": "ja" + }, + { + "@value": "Bileog agus Bileog", + "@language": "ga" + }, + { + "@value": "पत्ता रेक", + "@language": "hi" + }, + { + "@value": "Leaf Rake", + "@language": "zh" + }, + { + "@value": "Leaf Rake", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#machete", + "rdfs:label": [ + { + "@value": "Machette", + "@language": "fr" + }, + { + "@value": "Machete", + "@language": "es" + }, + { + "@value": "Machete", + "@language": "de" + }, + { + "@value": "Machete", + "@language": "en" + }, + { + "@value": "Machete", + "@language": "ar" + }, + { + "@value": "Machete", + "@language": "ku" + }, + { + "@value": "Machete", + "@language": "it" + }, + { + "@value": "Machete", + "@language": "sw" + }, + { + "@value": "Maquiagem", + "@language": "pt" + }, + { + "@value": "Machete", + "@language": "oc" + }, + { + "@value": "Мачет", + "@language": "ru" + }, + { + "@value": "Machete", + "@language": "cy" + }, + { + "@value": "マチェット", + "@language": "ja" + }, + { + "@value": "Déan teagmháil linn", + "@language": "ga" + }, + { + "@value": "मैथिली", + "@language": "hi" + }, + { + "@value": "Machete", + "@language": "zh" + }, + { + "@value": "Machete", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#cutting-tool", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#manual-edger", + "rdfs:label": [ + { + "@value": "Edger manuel", + "@language": "fr" + }, + { + "@value": "Edger manual", + "@language": "es" + }, + { + "@value": "Manueller Edger", + "@language": "de" + }, + { + "@value": "Manual Edger", + "@language": "en" + }, + { + "@value": "الدليل", + "@language": "ar" + }, + { + "@value": "Manual Edger", + "@language": "ku" + }, + { + "@value": "Bordo manuale", + "@language": "it" + }, + { + "@value": "Manual Edger", + "@language": "sw" + }, + { + "@value": "Borda manual", + "@language": "pt" + }, + { + "@value": "Manual Edger", + "@language": "oc" + }, + { + "@value": "Руководство Edger", + "@language": "ru" + }, + { + "@value": "Manual Edger", + "@language": "cy" + }, + { + "@value": "マニュアルエッジラー", + "@language": "ja" + }, + { + "@value": "Imeall Lámhleabhar", + "@language": "ga" + }, + { + "@value": "मैनुअल एडगर", + "@language": "hi" + }, + { + "@value": "手册", + "@language": "zh" + }, + { + "@value": "Manual Edger", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#pick-mattock", + "rdfs:label": [ + { + "@value": "Choisi le mittock", + "@language": "fr" + }, + { + "@value": "Pique Mattock", + "@language": "es" + }, + { + "@value": "Pick Mattock", + "@language": "de" + }, + { + "@value": "Pick Mattock", + "@language": "en" + }, + { + "@value": "اختار ماتوك", + "@language": "ar" + }, + { + "@value": "Pick Mattock", + "@language": "ku" + }, + { + "@value": "Scegli Mattock", + "@language": "it" + }, + { + "@value": "Pick Mattock", + "@language": "sw" + }, + { + "@value": "Escolher Mattock", + "@language": "pt" + }, + { + "@value": "Pick Mattock", + "@language": "oc" + }, + { + "@value": "Выберите Mattock", + "@language": "ru" + }, + { + "@value": "Pick Mattock", + "@language": "cy" + }, + { + "@value": "ピックマドック", + "@language": "ja" + }, + { + "@value": "Pioc Matter", + "@language": "ga" + }, + { + "@value": "पिका मैटॉक", + "@language": "hi" + }, + { + "@value": "Pick Mattock", + "@language": "zh" + }, + { + "@value": "Pick Mattock", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#pitchfork", + "rdfs:label": [ + { + "@value": "Pittoresque", + "@language": "fr" + }, + { + "@value": "Horca", + "@language": "es" + }, + { + "@value": "Heugabel", + "@language": "de" + }, + { + "@value": "Pitchfork", + "@language": "en" + }, + { + "@value": "Pitchfork", + "@language": "ar" + }, + { + "@value": "Pitchfork", + "@language": "ku" + }, + { + "@value": "Pitchfork", + "@language": "it" + }, + { + "@value": "Pitchfork", + "@language": "sw" + }, + { + "@value": "Pitchfork", + "@language": "pt" + }, + { + "@value": "Pitchfork", + "@language": "oc" + }, + { + "@value": "Питчфорк", + "@language": "ru" + }, + { + "@value": "Pitchfork", + "@language": "cy" + }, + { + "@value": "ピッチフォーク", + "@language": "ja" + }, + { + "@value": "Pitchfork", + "@language": "ga" + }, + { + "@value": "पिचफोर्क", + "@language": "hi" + }, + { + "@value": "Pitchfork", + "@language": "zh" + }, + { + "@value": "Pitchfork", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#pitchfork", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#planting-dibble", + "rdfs:label": [ + { + "@value": "Planter DiBble", + "@language": "fr" + }, + { + "@value": "Plantando Dibble", + "@language": "es" + }, + { + "@value": "Anpflanzung von Dibbeln", + "@language": "de" + }, + { + "@value": "Planting Dibble", + "@language": "en" + }, + { + "@value": "البلطجة", + "@language": "ar" + }, + { + "@value": "Planting Dibble", + "@language": "ku" + }, + { + "@value": "Piantare Dibble", + "@language": "it" + }, + { + "@value": "Planting Dibble", + "@language": "sw" + }, + { + "@value": "Dibble de plantação", + "@language": "pt" + }, + { + "@value": "Planting Dibble", + "@language": "oc" + }, + { + "@value": "Посадка Dibble", + "@language": "ru" + }, + { + "@value": "Planting Dibble", + "@language": "cy" + }, + { + "@value": "植物の石", + "@language": "ja" + }, + { + "@value": "Plandaí Dibble", + "@language": "ga" + }, + { + "@value": "रोपण डिबल", + "@language": "hi" + }, + { + "@value": "计划", + "@language": "zh" + }, + { + "@value": "Planting Dibble", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#pointed-shovel", + "rdfs:label": [ + { + "@value": "Pelle pointue", + "@language": "fr" + }, + { + "@value": "Pala puntiaguda", + "@language": "es" + }, + { + "@value": "Spitzer Schaufel", + "@language": "de" + }, + { + "@value": "Pointed Shovel", + "@language": "en" + }, + { + "@value": "شوفل", + "@language": "ar" + }, + { + "@value": "Pointed Shovel", + "@language": "ku" + }, + { + "@value": "Spiagge a punta", + "@language": "it" + }, + { + "@value": "Pointed Shovel", + "@language": "sw" + }, + { + "@value": "Tiro apontado", + "@language": "pt" + }, + { + "@value": "Pointed Shovel", + "@language": "oc" + }, + { + "@value": "Указанный Шовель", + "@language": "ru" + }, + { + "@value": "Pointed Shovel", + "@language": "cy" + }, + { + "@value": "尖ったショベル", + "@language": "ja" + }, + { + "@value": "Luaidhe Luaidhe", + "@language": "ga" + }, + { + "@value": "Pointed Shovel", + "@language": "hi" + }, + { + "@value": "B. 突出的防灾", + "@language": "zh" + }, + { + "@value": "Pointed Shovel", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#shovel", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#pole-pruner", + "rdfs:label": [ + { + "@value": "Châtrice", + "@language": "fr" + }, + { + "@value": "Pole Proper", + "@language": "es" + }, + { + "@value": "Pole Pruner", + "@language": "de" + }, + { + "@value": "Pole Pruner", + "@language": "en" + }, + { + "@value": "Pole Pruner", + "@language": "ar" + }, + { + "@value": "Pole Pruner", + "@language": "ku" + }, + { + "@value": "Pole Pruner", + "@language": "it" + }, + { + "@value": "Pole Pruner", + "@language": "sw" + }, + { + "@value": "Pólo Pruner", + "@language": "pt" + }, + { + "@value": "Pole Pruner", + "@language": "oc" + }, + { + "@value": "Полюс Pruner", + "@language": "ru" + }, + { + "@value": "Pole Pruner", + "@language": "cy" + }, + { + "@value": "ポーランド人のプルナー", + "@language": "ja" + }, + { + "@value": "Pole Pruner", + "@language": "ga" + }, + { + "@value": "पोल प्रूनर", + "@language": "hi" + }, + { + "@value": "Pruner", + "@language": "zh" + }, + { + "@value": "Pole Pruner", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#cutting-tool", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#post-hole-pincer", + "rdfs:label": [ + { + "@value": "Pincer Pincer", + "@language": "fr" + }, + { + "@value": "Publicar pinza de orificio", + "@language": "es" + }, + { + "@value": "Postloch-Zangen", + "@language": "de" + }, + { + "@value": "Post Hole Pincer", + "@language": "en" + }, + { + "@value": "Post Hole Pincer", + "@language": "ar" + }, + { + "@value": "Post Hole Pincer", + "@language": "ku" + }, + { + "@value": "Perno del foro del poster", + "@language": "it" + }, + { + "@value": "Post Hole Pincer", + "@language": "sw" + }, + { + "@value": "Pinças de furo de cartão", + "@language": "pt" + }, + { + "@value": "Post Hole Pincer", + "@language": "oc" + }, + { + "@value": "пост Hole Pincer", + "@language": "ru" + }, + { + "@value": "Post Hole Pincer", + "@language": "cy" + }, + { + "@value": "ポストの穴のPincer", + "@language": "ja" + }, + { + "@value": "cineál gas: in airde", + "@language": "ga" + }, + { + "@value": "पोस्ट होल पिनर", + "@language": "hi" + }, + { + "@value": "霍尔·普林科", + "@language": "zh" + }, + { + "@value": "Post Hole Pincer", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#potato-fork", + "rdfs:label": [ + { + "@value": "Fourchette de pommes de terre", + "@language": "fr" + }, + { + "@value": "Tenedor de patata", + "@language": "es" + }, + { + "@value": "Kartoffelgabel", + "@language": "de" + }, + { + "@value": "Potato Fork", + "@language": "en" + }, + { + "@value": "شوكة البطاطا", + "@language": "ar" + }, + { + "@value": "Potato Fork", + "@language": "ku" + }, + { + "@value": "Forcella di patate", + "@language": "it" + }, + { + "@value": "Potato Fork", + "@language": "sw" + }, + { + "@value": "Fork de batata", + "@language": "pt" + }, + { + "@value": "Potato Fork", + "@language": "oc" + }, + { + "@value": "Картофель Форк", + "@language": "ru" + }, + { + "@value": "Potato Fork", + "@language": "cy" + }, + { + "@value": "ポテトフォーク", + "@language": "ja" + }, + { + "@value": "Forcra Prátaí", + "@language": "ga" + }, + { + "@value": "आलू कांटा", + "@language": "hi" + }, + { + "@value": "B. 波塔什", + "@language": "zh" + }, + { + "@value": "Potato Fork", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#powered-chainsaw", + "rdfs:label": [ + { + "@value": "Tronçonneuse alimenté", + "@language": "fr" + }, + { + "@value": "Motosierra motorizada", + "@language": "es" + }, + { + "@value": "Angetriebene Kettensäge", + "@language": "de" + }, + { + "@value": "Powered Chainsaw", + "@language": "en" + }, + { + "@value": "Powered Chainsaw", + "@language": "ar" + }, + { + "@value": "Powered Chainsaw", + "@language": "ku" + }, + { + "@value": "motosega a catena", + "@language": "it" + }, + { + "@value": "Powered Chainsaw", + "@language": "sw" + }, + { + "@value": "Serra de corrente elétrica", + "@language": "pt" + }, + { + "@value": "Powered Chainsaw", + "@language": "oc" + }, + { + "@value": "Питаться Chainsaw", + "@language": "ru" + }, + { + "@value": "Powered Chainsaw", + "@language": "cy" + }, + { + "@value": "パワーチェーンソー", + "@language": "ja" + }, + { + "@value": "Slabhraí Powered", + "@language": "ga" + }, + { + "@value": "संचालित चेनसॉ", + "@language": "hi" + }, + { + "@value": "权力", + "@language": "zh" + }, + { + "@value": "Powered Chainsaw", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#saw", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#powered-edger", + "rdfs:label": [ + { + "@value": "Edger alimenté", + "@language": "fr" + }, + { + "@value": "Energista motorizado", + "@language": "es" + }, + { + "@value": "Angetriebener Edger", + "@language": "de" + }, + { + "@value": "Powered Edger", + "@language": "en" + }, + { + "@value": "Powered Edger", + "@language": "ar" + }, + { + "@value": "Powered Edger", + "@language": "ku" + }, + { + "@value": "Edger alimentato", + "@language": "it" + }, + { + "@value": "Powered Edger", + "@language": "sw" + }, + { + "@value": "Borda alimentado", + "@language": "pt" + }, + { + "@value": "Powered Edger", + "@language": "oc" + }, + { + "@value": "Работает Edger", + "@language": "ru" + }, + { + "@value": "Powered Edger", + "@language": "cy" + }, + { + "@value": "動力を与えられたEdger", + "@language": "ja" + }, + { + "@value": "Imeall cumhachtach", + "@language": "ga" + }, + { + "@value": "संचालित एडगर", + "@language": "hi" + }, + { + "@value": "权力", + "@language": "zh" + }, + { + "@value": "Powered Edger", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#pruning-knife", + "rdfs:label": [ + { + "@value": "Coup de couteau", + "@language": "fr" + }, + { + "@value": "Cuchillo de poda", + "@language": "es" + }, + { + "@value": "Krebsmesser", + "@language": "de" + }, + { + "@value": "Pruning Knife", + "@language": "en" + }, + { + "@value": "قنيفي", + "@language": "ar" + }, + { + "@value": "Pruning Knife", + "@language": "ku" + }, + { + "@value": "Coltello di potatura", + "@language": "it" + }, + { + "@value": "Pruning Knife", + "@language": "sw" + }, + { + "@value": "Faca de poda", + "@language": "pt" + }, + { + "@value": "Pruning Knife", + "@language": "oc" + }, + { + "@value": "Обрезая нож", + "@language": "ru" + }, + { + "@value": "Pruning Knife", + "@language": "cy" + }, + { + "@value": "剪定ナイフ", + "@language": "ja" + }, + { + "@value": "Scannáin a reáchtáil", + "@language": "ga" + }, + { + "@value": "चाकू", + "@language": "hi" + }, + { + "@value": "D. Knife", + "@language": "zh" + }, + { + "@value": "Pruning Knife", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#cutting-tool", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#pruning-saw", + "rdfs:label": [ + { + "@value": "Scie à élagité", + "@language": "fr" + }, + { + "@value": "Sierra de poda", + "@language": "es" + }, + { + "@value": "Beschneidungssäge", + "@language": "de" + }, + { + "@value": "Pruning Saw", + "@language": "en" + }, + { + "@value": "منشار", + "@language": "ar" + }, + { + "@value": "Pruning Saw", + "@language": "ku" + }, + { + "@value": "Sega di potatura", + "@language": "it" + }, + { + "@value": "Pruning Saw", + "@language": "sw" + }, + { + "@value": "Serra de Pruning", + "@language": "pt" + }, + { + "@value": "Pruning Saw", + "@language": "oc" + }, + { + "@value": "Служить пилу", + "@language": "ru" + }, + { + "@value": "Pruning Saw", + "@language": "cy" + }, + { + "@value": "剪定 見ました", + "@language": "ja" + }, + { + "@value": "Pruning Saw", + "@language": "ga" + }, + { + "@value": "प्रूनिंग सॉ", + "@language": "hi" + }, + { + "@value": "Prun Saw", + "@language": "zh" + }, + { + "@value": "Pruning Saw", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#saw", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#pruning-shears", + "rdfs:label": [ + { + "@value": "Cisailles de taille", + "@language": "fr" + }, + { + "@value": "Tijeras de podar", + "@language": "es" + }, + { + "@value": "Beschneidungsschere", + "@language": "de" + }, + { + "@value": "Pruning Shears", + "@language": "en" + }, + { + "@value": "الخناق", + "@language": "ar" + }, + { + "@value": "Pruning Shears", + "@language": "ku" + }, + { + "@value": "Pugnali di potatura", + "@language": "it" + }, + { + "@value": "Pruning Shears", + "@language": "sw" + }, + { + "@value": "Meias de poda", + "@language": "pt" + }, + { + "@value": "Pruning Shears", + "@language": "oc" + }, + { + "@value": "Обрезка шейар", + "@language": "ru" + }, + { + "@value": "Pruning Shears", + "@language": "cy" + }, + { + "@value": "剪定せん断", + "@language": "ja" + }, + { + "@value": "Seirbhísí a reáchtáil", + "@language": "ga" + }, + { + "@value": "प्रूनिंग शेर", + "@language": "hi" + }, + { + "@value": "Pruning Shears", + "@language": "zh" + }, + { + "@value": "Pruning Shears", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#cutting-tool", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#rake", + "rdfs:label": [ + { + "@value": "Râteau", + "@language": "fr" + }, + { + "@value": "Rastrillo", + "@language": "es" + }, + { + "@value": "Rechen", + "@language": "de" + }, + { + "@value": "Rake", + "@language": "en" + }, + { + "@value": "Rake", + "@language": "ar" + }, + { + "@value": "Rake", + "@language": "ku" + }, + { + "@value": "Rake", + "@language": "it" + }, + { + "@value": "Rake", + "@language": "sw" + }, + { + "@value": "Rake.", + "@language": "pt" + }, + { + "@value": "Rake", + "@language": "oc" + }, + { + "@value": "Рейк", + "@language": "ru" + }, + { + "@value": "Rake", + "@language": "cy" + }, + { + "@value": "ルーク", + "@language": "ja" + }, + { + "@value": "Ról an rabhla", + "@language": "ga" + }, + { + "@value": "रेक", + "@language": "hi" + }, + { + "@value": "Rake", + "@language": "zh" + }, + { + "@value": "Rake", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#rotary-tiller", + "rdfs:label": [ + { + "@value": "Taber rotatif", + "@language": "fr" + }, + { + "@value": "Timón rotatorio", + "@language": "es" + }, + { + "@value": "Rotary-Tiller", + "@language": "de" + }, + { + "@value": "Rotary Tiller", + "@language": "en" + }, + { + "@value": "طراز Rotary Tiller", + "@language": "ar" + }, + { + "@value": "Rotary Tiller", + "@language": "ku" + }, + { + "@value": "Timoneria rotante", + "@language": "it" + }, + { + "@value": "Rotary Tiller", + "@language": "sw" + }, + { + "@value": "Tiller rotativo", + "@language": "pt" + }, + { + "@value": "Rotary Tiller", + "@language": "oc" + }, + { + "@value": "Ротари Тиллер", + "@language": "ru" + }, + { + "@value": "Rotary Tiller", + "@language": "cy" + }, + { + "@value": "ロータリーチラー", + "@language": "ja" + }, + { + "@value": "Rothlach Tiller", + "@language": "ga" + }, + { + "@value": "रोटरी टिलर", + "@language": "hi" + }, + { + "@value": "罗特·特雷尔", + "@language": "zh" + }, + { + "@value": "Rotary Tiller", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#round-point-shovel", + "rdfs:label": [ + { + "@value": "Pelle à point rond", + "@language": "fr" + }, + { + "@value": "Punta redonda Pala", + "@language": "es" + }, + { + "@value": "Rundpunktschaufel", + "@language": "de" + }, + { + "@value": "Round Point Shovel", + "@language": "en" + }, + { + "@value": "الجولة", + "@language": "ar" + }, + { + "@value": "Round Point Shovel", + "@language": "ku" + }, + { + "@value": "Punta rotonda Shovel", + "@language": "it" + }, + { + "@value": "Round Point Shovel", + "@language": "sw" + }, + { + "@value": "Tiro de ponto redondo", + "@language": "pt" + }, + { + "@value": "Round Point Shovel", + "@language": "oc" + }, + { + "@value": "Круглый Точка Shovel", + "@language": "ru" + }, + { + "@value": "Round Point Shovel", + "@language": "cy" + }, + { + "@value": "ラウンドポイントシューベル", + "@language": "ja" + }, + { + "@value": "Bhabhta Pointe bróg", + "@language": "ga" + }, + { + "@value": "राउंड पॉइंट शोवेल", + "@language": "hi" + }, + { + "@value": "圆桌会议", + "@language": "zh" + }, + { + "@value": "Round Point Shovel", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#shovel", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#scoop-shovel", + "rdfs:label": [ + { + "@value": "Pelleteuse", + "@language": "fr" + }, + { + "@value": "Pala de scoel", + "@language": "es" + }, + { + "@value": "Scoop-Schaufel", + "@language": "de" + }, + { + "@value": "Scoop Shovel", + "@language": "en" + }, + { + "@value": "سكوبي شوفل", + "@language": "ar" + }, + { + "@value": "Scoop Shovel", + "@language": "ku" + }, + { + "@value": "Scoop Shovel", + "@language": "it" + }, + { + "@value": "Scoop Shovel", + "@language": "sw" + }, + { + "@value": "Tiro de colher", + "@language": "pt" + }, + { + "@value": "Scoop Shovel", + "@language": "oc" + }, + { + "@value": "Scoop Шовель", + "@language": "ru" + }, + { + "@value": "Scoop Shovel", + "@language": "cy" + }, + { + "@value": "スクープショベル", + "@language": "ja" + }, + { + "@value": "Scoop bróg", + "@language": "ga" + }, + { + "@value": "स्कूप फावड़ा", + "@language": "hi" + }, + { + "@value": "Scoop Shovel", + "@language": "zh" + }, + { + "@value": "Scoop Shovel", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#shovel", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#scuffle-hoe", + "rdfs:label": [ + { + "@value": "Bracelet houe", + "@language": "fr" + }, + { + "@value": "Azada", + "@language": "es" + }, + { + "@value": "Ruckhacke", + "@language": "de" + }, + { + "@value": "Scuffle Hoe", + "@language": "en" + }, + { + "@value": "Scuffle Hoe", + "@language": "ar" + }, + { + "@value": "Scuffle Hoe", + "@language": "ku" + }, + { + "@value": "Scuffo Hoe", + "@language": "it" + }, + { + "@value": "Scuffle Hoe", + "@language": "sw" + }, + { + "@value": "Scuffle Hoe", + "@language": "pt" + }, + { + "@value": "Scuffle Hoe", + "@language": "oc" + }, + { + "@value": "Скуфл Хой", + "@language": "ru" + }, + { + "@value": "Scuffle Hoe", + "@language": "cy" + }, + { + "@value": "スクラッチ ホエ", + "@language": "ja" + }, + { + "@value": "Scuffle taiseachas aeir: fliuch", + "@language": "ga" + }, + { + "@value": "झरना हो", + "@language": "hi" + }, + { + "@value": "Scuffle 霍 埃", + "@language": "zh" + }, + { + "@value": "Scuffle Hoe", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#gardening", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#seeder-row-planter", + "rdfs:label": [ + { + "@value": "Planteur de rangée de semis", + "@language": "fr" + }, + { + "@value": "Sembradora de fila", + "@language": "es" + }, + { + "@value": "Sämaschine Reihenpflanzer", + "@language": "de" + }, + { + "@value": "Seeder Row Planter", + "@language": "en" + }, + { + "@value": "طراز Seeder Row Planter", + "@language": "ar" + }, + { + "@value": "Seeder Row Planter", + "@language": "ku" + }, + { + "@value": "Fioriera di fila del Seeder", + "@language": "it" + }, + { + "@value": "Seeder Row Planter", + "@language": "sw" + }, + { + "@value": "Plantador de linha de semeadura", + "@language": "pt" + }, + { + "@value": "Seeder Row Planter", + "@language": "oc" + }, + { + "@value": "Сеялка Row Planter", + "@language": "ru" + }, + { + "@value": "Seeder Row Planter", + "@language": "cy" + }, + { + "@value": "シーダー・ロウ・プランター", + "@language": "ja" + }, + { + "@value": "Plandaí faoi dhíon", + "@language": "ga" + }, + { + "@value": "सीडर रो प्लेंटर", + "@language": "hi" + }, + { + "@value": "见der Rowter计划", + "@language": "zh" + }, + { + "@value": "Seeder Row Planter", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#seeder-row-planter", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#shredder", + "rdfs:label": [ + { + "@value": "Déchiqueteuse", + "@language": "fr" + }, + { + "@value": "Desfibradora", + "@language": "es" + }, + { + "@value": "Shredder", + "@language": "de" + }, + { + "@value": "Shredder", + "@language": "en" + }, + { + "@value": "ممزق", + "@language": "ar" + }, + { + "@value": "Shredder", + "@language": "ku" + }, + { + "@value": "Trituratore", + "@language": "it" + }, + { + "@value": "Shredder", + "@language": "sw" + }, + { + "@value": "Shredder", + "@language": "pt" + }, + { + "@value": "Shredder", + "@language": "oc" + }, + { + "@value": "Шредер", + "@language": "ru" + }, + { + "@value": "Shredder", + "@language": "cy" + }, + { + "@value": "シュレッダー", + "@language": "ja" + }, + { + "@value": "taiseachas aeir: fliuch", + "@language": "ga" + }, + { + "@value": "श्रेडर", + "@language": "hi" + }, + { + "@value": "希 德", + "@language": "zh" + }, + { + "@value": "Shredder", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#shredder", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#soil-scoop", + "rdfs:label": [ + { + "@value": "Écosserie", + "@language": "fr" + }, + { + "@value": "Cucharada de suelo", + "@language": "es" + }, + { + "@value": "Bodenschaufel", + "@language": "de" + }, + { + "@value": "Soil Scoop", + "@language": "en" + }, + { + "@value": "Soil Scoop", + "@language": "ar" + }, + { + "@value": "Soil Scoop", + "@language": "ku" + }, + { + "@value": "Soil Scoop", + "@language": "it" + }, + { + "@value": "Soil Scoop", + "@language": "sw" + }, + { + "@value": "Escopo de solo", + "@language": "pt" + }, + { + "@value": "Soil Scoop", + "@language": "oc" + }, + { + "@value": "Соил Scoop", + "@language": "ru" + }, + { + "@value": "Soil Scoop", + "@language": "cy" + }, + { + "@value": "土壌スクープ", + "@language": "ja" + }, + { + "@value": "Scoop ithreach", + "@language": "ga" + }, + { + "@value": "मृदा स्कूप", + "@language": "hi" + }, + { + "@value": "土壤 Scoop", + "@language": "zh" + }, + { + "@value": "Soil Scoop", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#soil-scoop", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#spading-fork", + "rdfs:label": [ + { + "@value": "Fourchette", + "@language": "fr" + }, + { + "@value": "Bifurcación", + "@language": "es" + }, + { + "@value": "Spading Gabel", + "@language": "de" + }, + { + "@value": "Spading Fork", + "@language": "en" + }, + { + "@value": "الشوكة العنكبوتية", + "@language": "ar" + }, + { + "@value": "Spading Fork", + "@language": "ku" + }, + { + "@value": "Forcella di spacco", + "@language": "it" + }, + { + "@value": "Spading Fork", + "@language": "sw" + }, + { + "@value": "Forqueta de Spading", + "@language": "pt" + }, + { + "@value": "Spading Fork", + "@language": "oc" + }, + { + "@value": "Спайдинг Fork", + "@language": "ru" + }, + { + "@value": "Spading Fork", + "@language": "cy" + }, + { + "@value": "スペーディングフォーク", + "@language": "ja" + }, + { + "@value": "Seirbhís do Chustaiméirí", + "@language": "ga" + }, + { + "@value": "स्पैडिंग फोर्क", + "@language": "hi" + }, + { + "@value": "投 票", + "@language": "zh" + }, + { + "@value": "Spading Fork", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#spading-fork", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#spike-aerator", + "rdfs:label": [ + { + "@value": "Aérateur de pic", + "@language": "fr" + }, + { + "@value": "Aireador de espiga", + "@language": "es" + }, + { + "@value": "Spitzenbelüfter", + "@language": "de" + }, + { + "@value": "Spike Aerator", + "@language": "en" + }, + { + "@value": "Spike Aerator", + "@language": "ar" + }, + { + "@value": "Spike Aerator", + "@language": "ku" + }, + { + "@value": "Spike Aerator", + "@language": "it" + }, + { + "@value": "Spike Aerator", + "@language": "sw" + }, + { + "@value": "Aerador de Spike", + "@language": "pt" + }, + { + "@value": "Spike Aerator", + "@language": "oc" + }, + { + "@value": "Шоу-Аэратор", + "@language": "ru" + }, + { + "@value": "Spike Aerator", + "@language": "cy" + }, + { + "@value": "スパイク・アレイター", + "@language": "ja" + }, + { + "@value": "Spike Aerárthaí", + "@language": "ga" + }, + { + "@value": "स्पाइक एरेटर", + "@language": "hi" + }, + { + "@value": "发言人", + "@language": "zh" + }, + { + "@value": "Spike Aerator", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#spike-aerator", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#spiked-aerating-shoes", + "rdfs:label": [ + { + "@value": "Chaussures d'aération à pointes", + "@language": "fr" + }, + { + "@value": "Zapatos de aire acera con pinchos", + "@language": "es" + }, + { + "@value": "Spitzen belüftete Schuhe", + "@language": "de" + }, + { + "@value": "Spiked Aerating Shoes", + "@language": "en" + }, + { + "@value": "Spiked Aerating Shoes", + "@language": "ar" + }, + { + "@value": "Spiked Aerating Shoes", + "@language": "ku" + }, + { + "@value": "Spike Aerating Scarpe", + "@language": "it" + }, + { + "@value": "Spiked Aerating Shoes", + "@language": "sw" + }, + { + "@value": "Sapatos de aerar cravados", + "@language": "pt" + }, + { + "@value": "Spiked Aerating Shoes", + "@language": "oc" + }, + { + "@value": "Spiked Aerating Обувь", + "@language": "ru" + }, + { + "@value": "Spiked Aerating Shoes", + "@language": "cy" + }, + { + "@value": "スパイクされた食感の靴", + "@language": "ja" + }, + { + "@value": "Bróga Aerála Spiked", + "@language": "ga" + }, + { + "@value": "Spiked Aerating जूते", + "@language": "hi" + }, + { + "@value": "投 票", + "@language": "zh" + }, + { + "@value": "Spiked Aerating Shoes", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#shoes", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#spreader", + "rdfs:label": [ + { + "@value": "Épandeuse", + "@language": "fr" + }, + { + "@value": "Esparcidor", + "@language": "es" + }, + { + "@value": "Streuer", + "@language": "de" + }, + { + "@value": "Spreader", + "@language": "en" + }, + { + "@value": "مُخَرِّب", + "@language": "ar" + }, + { + "@value": "Spreader", + "@language": "ku" + }, + { + "@value": "Spreader", + "@language": "it" + }, + { + "@value": "Spreader", + "@language": "sw" + }, + { + "@value": "Espalhador", + "@language": "pt" + }, + { + "@value": "Spreader", + "@language": "oc" + }, + { + "@value": "Спред", + "@language": "ru" + }, + { + "@value": "Spreader", + "@language": "cy" + }, + { + "@value": "スプレッド", + "@language": "ja" + }, + { + "@value": "Scaipeadh", + "@language": "ga" + }, + { + "@value": "स्प्रेडर", + "@language": "hi" + }, + { + "@value": "版本", + "@language": "zh" + }, + { + "@value": "Spreader", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#spreader", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#sprinkler", + "rdfs:label": [ + { + "@value": "Arroseuse", + "@language": "fr" + }, + { + "@value": "Aspersor", + "@language": "es" + }, + { + "@value": "Sprinkler", + "@language": "de" + }, + { + "@value": "Sprinkler", + "@language": "en" + }, + { + "@value": "Sprinkler", + "@language": "ar" + }, + { + "@value": "Sprinkler", + "@language": "ku" + }, + { + "@value": "Sprinkler", + "@language": "it" + }, + { + "@value": "Sprinkler", + "@language": "sw" + }, + { + "@value": "Aspersores", + "@language": "pt" + }, + { + "@value": "Sprinkler", + "@language": "oc" + }, + { + "@value": "Спринклер", + "@language": "ru" + }, + { + "@value": "Sprinkler", + "@language": "cy" + }, + { + "@value": "スプリンクラー", + "@language": "ja" + }, + { + "@value": "Seirbhís do Chustaiméirí", + "@language": "ga" + }, + { + "@value": "छिड़काव", + "@language": "hi" + }, + { + "@value": "减少", + "@language": "zh" + }, + { + "@value": "Sprinkler", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#sprinkler", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#square-point-shovel", + "rdfs:label": [ + { + "@value": "Pelle à point carré", + "@language": "fr" + }, + { + "@value": "Pala de punto cuadrado", + "@language": "es" + }, + { + "@value": "Quadratpunktschaufel", + "@language": "de" + }, + { + "@value": "Square Point Shovel", + "@language": "en" + }, + { + "@value": "Square Point Shovel", + "@language": "ar" + }, + { + "@value": "Square Point Shovel", + "@language": "ku" + }, + { + "@value": "Puntale quadrato Shovel", + "@language": "it" + }, + { + "@value": "Square Point Shovel", + "@language": "sw" + }, + { + "@value": "Ponto quadrado Shovel", + "@language": "pt" + }, + { + "@value": "Square Point Shovel", + "@language": "oc" + }, + { + "@value": "Квадратный Точка Шовель", + "@language": "ru" + }, + { + "@value": "Square Point Shovel", + "@language": "cy" + }, + { + "@value": "スクエアポイントショベル", + "@language": "ja" + }, + { + "@value": "Cearnóg Point bróg", + "@language": "ga" + }, + { + "@value": "स्क्वायर प्वाइंट फावड़ा", + "@language": "hi" + }, + { + "@value": "Square Point Shovel", + "@language": "zh" + }, + { + "@value": "Square Point Shovel", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#shovel", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#step-edger", + "rdfs:label": [ + { + "@value": "Step Edger", + "@language": "fr" + }, + { + "@value": "Edger Paso", + "@language": "es" + }, + { + "@value": "Schritt Edger", + "@language": "de" + }, + { + "@value": "Step Edger", + "@language": "en" + }, + { + "@value": "Step Edger", + "@language": "ar" + }, + { + "@value": "Step Edger", + "@language": "ku" + }, + { + "@value": "Fase Edger", + "@language": "it" + }, + { + "@value": "Step Edger", + "@language": "sw" + }, + { + "@value": "Passo Edger", + "@language": "pt" + }, + { + "@value": "Step Edger", + "@language": "oc" + }, + { + "@value": "Шаг Edger", + "@language": "ru" + }, + { + "@value": "Step Edger", + "@language": "cy" + }, + { + "@value": "ステップエッジ", + "@language": "ja" + }, + { + "@value": "Céim Edge", + "@language": "ga" + }, + { + "@value": "स्टेप एडगर", + "@language": "hi" + }, + { + "@value": "步骤Edger", + "@language": "zh" + }, + { + "@value": "Step Edger", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#step-edger", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#string-trimmer", + "rdfs:label": [ + { + "@value": "Tondeuse", + "@language": "fr" + }, + { + "@value": "Cortadora de cuerdas", + "@language": "es" + }, + { + "@value": "String-Trimmer", + "@language": "de" + }, + { + "@value": "String Trimmer", + "@language": "en" + }, + { + "@value": "String Trimmer", + "@language": "ar" + }, + { + "@value": "String Trimmer", + "@language": "ku" + }, + { + "@value": "Trimmer di stringa", + "@language": "it" + }, + { + "@value": "String Trimmer", + "@language": "sw" + }, + { + "@value": "Aparador de corda", + "@language": "pt" + }, + { + "@value": "String Trimmer", + "@language": "oc" + }, + { + "@value": "Струнный триммер", + "@language": "ru" + }, + { + "@value": "String Trimmer", + "@language": "cy" + }, + { + "@value": "弦トリマー", + "@language": "ja" + }, + { + "@value": "Teaghrán Trimmer", + "@language": "ga" + }, + { + "@value": "स्ट्रिंग ट्रिमर", + "@language": "hi" + }, + { + "@value": "2. 装甲车", + "@language": "zh" + }, + { + "@value": "String Trimmer", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#string-trimmer", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#trailer-sprayer", + "rdfs:label": [ + { + "@value": "Pulvérisateur de remorque", + "@language": "fr" + }, + { + "@value": "Pulverizador de remolque", + "@language": "es" + }, + { + "@value": "Anhängersprüher", + "@language": "de" + }, + { + "@value": "Trailer Sprayer", + "@language": "en" + }, + { + "@value": "مقطورة", + "@language": "ar" + }, + { + "@value": "Trailer Sprayer", + "@language": "ku" + }, + { + "@value": "Spruzzatore di rimorchio", + "@language": "it" + }, + { + "@value": "Trailer Sprayer", + "@language": "sw" + }, + { + "@value": "Pulverizador de reboque", + "@language": "pt" + }, + { + "@value": "Trailer Sprayer", + "@language": "oc" + }, + { + "@value": "Трейлер Sprayer", + "@language": "ru" + }, + { + "@value": "Trailer Sprayer", + "@language": "cy" + }, + { + "@value": "トレーラーのスプレーヤー", + "@language": "ja" + }, + { + "@value": "Seirbhís do Chustaiméirí", + "@language": "ga" + }, + { + "@value": "ट्रेलर स्प्रेयर", + "@language": "hi" + }, + { + "@value": "铁路喷雾器", + "@language": "zh" + }, + { + "@value": "Trailer Sprayer", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#sprayer", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#transplant-spade", + "rdfs:label": [ + { + "@value": "Pache de transplantation", + "@language": "fr" + }, + { + "@value": "Espada de trasplante", + "@language": "es" + }, + { + "@value": "Spaten transplantieren", + "@language": "de" + }, + { + "@value": "Transplant Spade", + "@language": "en" + }, + { + "@value": "زرع سبايد", + "@language": "ar" + }, + { + "@value": "Transplant Spade", + "@language": "ku" + }, + { + "@value": "Spazzola trapianta", + "@language": "it" + }, + { + "@value": "Transplant Spade", + "@language": "sw" + }, + { + "@value": "Spade de Transplante", + "@language": "pt" + }, + { + "@value": "Transplant Spade", + "@language": "oc" + }, + { + "@value": "Transplant Спайд", + "@language": "ru" + }, + { + "@value": "Transplant Spade", + "@language": "cy" + }, + { + "@value": "トランスプラントスパード", + "@language": "ja" + }, + { + "@value": "Trasphlandú Spa", + "@language": "ga" + }, + { + "@value": "ट्रांसप्लांट स्पाइड", + "@language": "hi" + }, + { + "@value": "移植剂", + "@language": "zh" + }, + { + "@value": "Transplant Spade", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#shovel", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#trench-shovel", + "rdfs:label": [ + { + "@value": "Tranchée", + "@language": "fr" + }, + { + "@value": "Pala de zanjas", + "@language": "es" + }, + { + "@value": "Grabenschaufel", + "@language": "de" + }, + { + "@value": "Trench Shovel", + "@language": "en" + }, + { + "@value": "Trench Shovel", + "@language": "ar" + }, + { + "@value": "Trench Shovel", + "@language": "ku" + }, + { + "@value": "Trench Shovel", + "@language": "it" + }, + { + "@value": "Trench Shovel", + "@language": "sw" + }, + { + "@value": "Tiro de trincheira", + "@language": "pt" + }, + { + "@value": "Trench Shovel", + "@language": "oc" + }, + { + "@value": "Тренч Шовель", + "@language": "ru" + }, + { + "@value": "Trench Shovel", + "@language": "cy" + }, + { + "@value": "トレンチ・ショベル", + "@language": "ja" + }, + { + "@value": "Trench bróg", + "@language": "ga" + }, + { + "@value": "ट्रेंच शोवेल", + "@language": "hi" + }, + { + "@value": "3. 特雷克·舒林", + "@language": "zh" + }, + { + "@value": "Trench Shovel", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#shovel", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#tree-pruner", + "rdfs:label": [ + { + "@value": "Arbre", + "@language": "fr" + }, + { + "@value": "Podadora de árboles", + "@language": "es" + }, + { + "@value": "Baum-Pruner", + "@language": "de" + }, + { + "@value": "Tree Pruner", + "@language": "en" + }, + { + "@value": "شجرة برونر", + "@language": "ar" + }, + { + "@value": "Tree Pruner", + "@language": "ku" + }, + { + "@value": "Albero Pruner", + "@language": "it" + }, + { + "@value": "Tree Pruner", + "@language": "sw" + }, + { + "@value": "Pingente de árvore", + "@language": "pt" + }, + { + "@value": "Tree Pruner", + "@language": "oc" + }, + { + "@value": "Дерево Pruner", + "@language": "ru" + }, + { + "@value": "Tree Pruner", + "@language": "cy" + }, + { + "@value": "ツリープルナー", + "@language": "ja" + }, + { + "@value": "cliceáil grianghraf a mhéadú", + "@language": "ga" + }, + { + "@value": "ट्री प्रूनर", + "@language": "hi" + }, + { + "@value": "特雷恩·佩雷", + "@language": "zh" + }, + { + "@value": "Tree Pruner", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#tree-pruner", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#twist-tiller", + "rdfs:label": [ + { + "@value": "Torsadeur", + "@language": "fr" + }, + { + "@value": "Timón de giro", + "@language": "es" + }, + { + "@value": "Twist-Tiller", + "@language": "de" + }, + { + "@value": "Twist Tiller", + "@language": "en" + }, + { + "@value": "Twist Tiller", + "@language": "ar" + }, + { + "@value": "Twist Tiller", + "@language": "ku" + }, + { + "@value": "Twist Tiller", + "@language": "it" + }, + { + "@value": "Twist Tiller", + "@language": "sw" + }, + { + "@value": "Twist Tiller", + "@language": "pt" + }, + { + "@value": "Twist Tiller", + "@language": "oc" + }, + { + "@value": "Твист Тиллер", + "@language": "ru" + }, + { + "@value": "Twist Tiller", + "@language": "cy" + }, + { + "@value": "ツイスト・チラー", + "@language": "ja" + }, + { + "@value": "Twist Tiller", + "@language": "ga" + }, + { + "@value": "ट्विस्ट टिलर", + "@language": "hi" + }, + { + "@value": "Twist iller", + "@language": "zh" + }, + { + "@value": "Twist Tiller", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#twist-tiller", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#warren-hoe", + "rdfs:label": [ + { + "@value": "Warren houe", + "@language": "fr" + }, + { + "@value": "Warren Hoe", + "@language": "es" + }, + { + "@value": "Warren Hoe", + "@language": "de" + }, + { + "@value": "Warren Hoe", + "@language": "en" + }, + { + "@value": "Warren Hoe", + "@language": "ar" + }, + { + "@value": "Warren Hoe", + "@language": "ku" + }, + { + "@value": "Warren Hoe", + "@language": "it" + }, + { + "@value": "Warren Hoe", + "@language": "sw" + }, + { + "@value": "Warren Hoe", + "@language": "pt" + }, + { + "@value": "Warren Hoe", + "@language": "oc" + }, + { + "@value": "Уоррен Хоэ", + "@language": "ru" + }, + { + "@value": "Warren Hoe", + "@language": "cy" + }, + { + "@value": "ウォーレン・ホー", + "@language": "ja" + }, + { + "@value": "cliceáil grianghraf a mhéadú", + "@language": "ga" + }, + { + "@value": "वॉरेन हो", + "@language": "hi" + }, + { + "@value": "2. 战争儿童", + "@language": "zh" + }, + { + "@value": "Warren Hoe", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#warren-hoe", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#water-hose", + "rdfs:label": [ + { + "@value": "Tuyau d'eau", + "@language": "fr" + }, + { + "@value": "Manguera de agua", + "@language": "es" + }, + { + "@value": "Wasserschlauch", + "@language": "de" + }, + { + "@value": "Water Hose", + "@language": "en" + }, + { + "@value": "المياه", + "@language": "ar" + }, + { + "@value": "Water Hose", + "@language": "ku" + }, + { + "@value": "Tubo dell'acqua", + "@language": "it" + }, + { + "@value": "Water Hose", + "@language": "sw" + }, + { + "@value": "Mangueira de água", + "@language": "pt" + }, + { + "@value": "Water Hose", + "@language": "oc" + }, + { + "@value": "Водный шланг", + "@language": "ru" + }, + { + "@value": "Water Hose", + "@language": "cy" + }, + { + "@value": "水ホース", + "@language": "ja" + }, + { + "@value": "taiseachas aeir: fliuch", + "@language": "ga" + }, + { + "@value": "पानी नली", + "@language": "hi" + }, + { + "@value": "水", + "@language": "zh" + }, + { + "@value": "Water Hose", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#water-hose", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#weeder", + "rdfs:label": [ + { + "@value": "Weeder", + "@language": "fr" + }, + { + "@value": "Weeder", + "@language": "es" + }, + { + "@value": "Weeder", + "@language": "de" + }, + { + "@value": "Weeder", + "@language": "en" + }, + { + "@value": "Weeder", + "@language": "ar" + }, + { + "@value": "Weeder", + "@language": "ku" + }, + { + "@value": "Weeder", + "@language": "it" + }, + { + "@value": "Weeder", + "@language": "sw" + }, + { + "@value": "Weeder", + "@language": "pt" + }, + { + "@value": "Weeder", + "@language": "oc" + }, + { + "@value": "Ведер", + "@language": "ru" + }, + { + "@value": "Weeder", + "@language": "cy" + }, + { + "@value": "ウェザー", + "@language": "ja" + }, + { + "@value": "Sraith an Domhain", + "@language": "ga" + }, + { + "@value": "वेडर", + "@language": "hi" + }, + { + "@value": "我们", + "@language": "zh" + }, + { + "@value": "Weeder", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#weeder", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#wheelbarrow", + "rdfs:label": [ + { + "@value": "Brouette", + "@language": "fr" + }, + { + "@value": "Carretilla", + "@language": "es" + }, + { + "@value": "Schubkarre", + "@language": "de" + }, + { + "@value": "Wheelbarrow", + "@language": "en" + }, + { + "@value": "Wheelbarrow", + "@language": "ar" + }, + { + "@value": "Wheelbarrow", + "@language": "ku" + }, + { + "@value": "Carrozzeria", + "@language": "it" + }, + { + "@value": "Wheelbarrow", + "@language": "sw" + }, + { + "@value": "Roda dentada", + "@language": "pt" + }, + { + "@value": "Wheelbarrow", + "@language": "oc" + }, + { + "@value": "Колесная стрелка", + "@language": "ru" + }, + { + "@value": "Wheelbarrow", + "@language": "cy" + }, + { + "@value": "ホイールバロー", + "@language": "ja" + }, + { + "@value": "tréimhse saoil: ilbhliantúil", + "@language": "ga" + }, + { + "@value": "Wheelbarrow", + "@language": "hi" + }, + { + "@value": "Wheelbile", + "@language": "zh" + }, + { + "@value": "Wheelbarrow", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#wheelbarrow", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#wheel-edger", + "rdfs:label": [ + { + "@value": "Edger de roue", + "@language": "fr" + }, + { + "@value": "Borde de la rueda", + "@language": "es" + }, + { + "@value": "Rollgedger", + "@language": "de" + }, + { + "@value": "Wheel Edger", + "@language": "en" + }, + { + "@value": "Wheel Edger", + "@language": "ar" + }, + { + "@value": "Wheel Edger", + "@language": "ku" + }, + { + "@value": "Bordatura della ruota", + "@language": "it" + }, + { + "@value": "Wheel Edger", + "@language": "sw" + }, + { + "@value": "Borda de roda", + "@language": "pt" + }, + { + "@value": "Wheel Edger", + "@language": "oc" + }, + { + "@value": "Колесо Edger", + "@language": "ru" + }, + { + "@value": "Wheel Edger", + "@language": "cy" + }, + { + "@value": "ホイールエッジ", + "@language": "ja" + }, + { + "@value": "Roth Edger", + "@language": "ga" + }, + { + "@value": "व्हील एडगर", + "@language": "hi" + }, + { + "@value": "Wheel Edger", + "@language": "zh" + }, + { + "@value": "Wheel Edger", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#wheel-edger", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#laser-cutter", + "rdfs:label": [ + { + "@value": "Coupeur laser", + "@language": "fr" + }, + { + "@value": "Cortador láser", + "@language": "es" + }, + { + "@value": "Laserschneider", + "@language": "de" + }, + { + "@value": "Laser Cutter", + "@language": "en" + }, + { + "@value": "Laser Cutter", + "@language": "ar" + }, + { + "@value": "Laser Cutter", + "@language": "ku" + }, + { + "@value": "Cutter laser", + "@language": "it" + }, + { + "@value": "Laser Cutter", + "@language": "sw" + }, + { + "@value": "Cortador de laser", + "@language": "pt" + }, + { + "@value": "Laser Cutter", + "@language": "oc" + }, + { + "@value": "Лазерный резак", + "@language": "ru" + }, + { + "@value": "Laser Cutter", + "@language": "cy" + }, + { + "@value": "レーザーカッター", + "@language": "ja" + }, + { + "@value": "Cutter Laser", + "@language": "ga" + }, + { + "@value": "लेजर कटर", + "@language": "hi" + }, + { + "@value": "Laser Cutter", + "@language": "zh" + }, + { + "@value": "Laser Cutter", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#laser-cutter", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#bandsaw", + "rdfs:label": [ + { + "@value": "Scie à ruban", + "@language": "fr" + }, + { + "@value": "Sierra de banda", + "@language": "es" + }, + { + "@value": "Bandsäge", + "@language": "de" + }, + { + "@value": "Bandsaw", + "@language": "en" + }, + { + "@value": "Bandsaw", + "@language": "ar" + }, + { + "@value": "Bandsaw", + "@language": "ku" + }, + { + "@value": "Sesso di banda", + "@language": "it" + }, + { + "@value": "Bandsaw", + "@language": "sw" + }, + { + "@value": "Serra de fita", + "@language": "pt" + }, + { + "@value": "Bandsaw", + "@language": "oc" + }, + { + "@value": "Бандау", + "@language": "ru" + }, + { + "@value": "Bandsaw", + "@language": "cy" + }, + { + "@value": "バンドソー", + "@language": "ja" + }, + { + "@value": "Banda", + "@language": "ga" + }, + { + "@value": "बैंडसॉ", + "@language": "hi" + }, + { + "@value": "潘基文", + "@language": "zh" + }, + { + "@value": "Bandsaw", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#saw", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#bench-grinder", + "rdfs:label": [ + { + "@value": "Broyeur", + "@language": "fr" + }, + { + "@value": "Amoladora de banco", + "@language": "es" + }, + { + "@value": "Benchsühle", + "@language": "de" + }, + { + "@value": "Bench Grinder", + "@language": "en" + }, + { + "@value": "Bench Grinder", + "@language": "ar" + }, + { + "@value": "Bench Grinder", + "@language": "ku" + }, + { + "@value": "Smerigliatrice di banco", + "@language": "it" + }, + { + "@value": "Bench Grinder", + "@language": "sw" + }, + { + "@value": "Moedor de bancada", + "@language": "pt" + }, + { + "@value": "Bench Grinder", + "@language": "oc" + }, + { + "@value": "Бенч Гриндер", + "@language": "ru" + }, + { + "@value": "Bench Grinder", + "@language": "cy" + }, + { + "@value": "ベンチの粉砕機", + "@language": "ja" + }, + { + "@value": "Seirbhís do Chustaiméirí", + "@language": "ga" + }, + { + "@value": "बेंच ग्राइंडर", + "@language": "hi" + }, + { + "@value": "现任主席", + "@language": "zh" + }, + { + "@value": "Bench Grinder", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#bench-grinder", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#bench-scroll-saw", + "rdfs:label": [ + { + "@value": "Scie de défilement bancaire", + "@language": "fr" + }, + { + "@value": "Sierra de desplazamiento del banco", + "@language": "es" + }, + { + "@value": "Bench Scroll Saw", + "@language": "de" + }, + { + "@value": "Bench Scroll Saw", + "@language": "en" + }, + { + "@value": "Bench Scroll Saw", + "@language": "ar" + }, + { + "@value": "Bench Scroll Saw", + "@language": "ku" + }, + { + "@value": "Scorrelazione del banco Sega", + "@language": "it" + }, + { + "@value": "Bench Scroll Saw", + "@language": "sw" + }, + { + "@value": "Rolo de banco Serra", + "@language": "pt" + }, + { + "@value": "Bench Scroll Saw", + "@language": "oc" + }, + { + "@value": "Бенч Scroll Пила", + "@language": "ru" + }, + { + "@value": "Bench Scroll Saw", + "@language": "cy" + }, + { + "@value": "ベンチスクロール 見ました", + "@language": "ja" + }, + { + "@value": "Scrollaigh Bench Sábháil", + "@language": "ga" + }, + { + "@value": "बेंच स्क्रॉल देखा", + "@language": "hi" + }, + { + "@value": "B. 现任职务 Saw", + "@language": "zh" + }, + { + "@value": "Bench Scroll Saw", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#saw", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#cnc-mill", + "rdfs:label": [ + { + "@value": "Moulin à commande numérique", + "@language": "fr" + }, + { + "@value": "Molino de cnc", + "@language": "es" + }, + { + "@value": "CNC-Mühle", + "@language": "de" + }, + { + "@value": "CNC Mill", + "@language": "en" + }, + { + "@value": "CNC ميل", + "@language": "ar" + }, + { + "@value": "CNC Mill", + "@language": "ku" + }, + { + "@value": "CNC Mulino", + "@language": "it" + }, + { + "@value": "CNC Mill", + "@language": "sw" + }, + { + "@value": "CNC Moinho", + "@language": "pt" + }, + { + "@value": "CNC Mill", + "@language": "oc" + }, + { + "@value": "ЧПУ Милли", + "@language": "ru" + }, + { + "@value": "CNC Mill", + "@language": "cy" + }, + { + "@value": "精密CNC ミルトン", + "@language": "ja" + }, + { + "@value": "CNC meaisínithe An Mhuilinn", + "@language": "ga" + }, + { + "@value": "सीएनसी मिल", + "@language": "hi" + }, + { + "@value": "刚果 M. Mill", + "@language": "zh" + }, + { + "@value": "CNC Mill", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#cnc-mill", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#dremels", + "rdfs:label": [ + { + "@value": "Dramelle", + "@language": "fr" + }, + { + "@value": "Dremels", + "@language": "es" + }, + { + "@value": "Bande", + "@language": "de" + }, + { + "@value": "Dremels", + "@language": "en" + }, + { + "@value": "Dremels", + "@language": "ar" + }, + { + "@value": "Dremels", + "@language": "ku" + }, + { + "@value": "Dremels", + "@language": "it" + }, + { + "@value": "Dremels", + "@language": "sw" + }, + { + "@value": "Dremels", + "@language": "pt" + }, + { + "@value": "Dremels", + "@language": "oc" + }, + { + "@value": "Дремели", + "@language": "ru" + }, + { + "@value": "Dremels", + "@language": "cy" + }, + { + "@value": "ドレメル", + "@language": "ja" + }, + { + "@value": "Inis dúinn, le do thoil...", + "@language": "ga" + }, + { + "@value": "Dremel", + "@language": "hi" + }, + { + "@value": "Dremels", + "@language": "zh" + }, + { + "@value": "Dremels", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#dremels", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#floor-standing-pillar-drill", + "rdfs:label": [ + { + "@value": "Porte debout au sol", + "@language": "fr" + }, + { + "@value": "Taladro de pilar de pie", + "@language": "es" + }, + { + "@value": "Bodenstehender Säulenbohrer", + "@language": "de" + }, + { + "@value": "Floor Standing Pillar Drill", + "@language": "en" + }, + { + "@value": "Floor Standing Pillar Drill", + "@language": "ar" + }, + { + "@value": "Floor Standing Pillar Drill", + "@language": "ku" + }, + { + "@value": "Pavimenti in piedi trapano pilastro", + "@language": "it" + }, + { + "@value": "Floor Standing Pillar Drill", + "@language": "sw" + }, + { + "@value": "Máquina de perfuração de piso", + "@language": "pt" + }, + { + "@value": "Floor Standing Pillar Drill", + "@language": "oc" + }, + { + "@value": "Пол Стендинг Pillar дрель", + "@language": "ru" + }, + { + "@value": "Floor Standing Pillar Drill", + "@language": "cy" + }, + { + "@value": "床の永続的な柱のドリル", + "@language": "ja" + }, + { + "@value": "Urlár Buan Druileáil Pillar", + "@language": "ga" + }, + { + "@value": "तल स्थायी स्तंभ ड्रिल", + "@language": "hi" + }, + { + "@value": "Floor常住Pillar Drill", + "@language": "zh" + }, + { + "@value": "Floor Standing Pillar Drill", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#drill", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#lathe", + "rdfs:label": [ + { + "@value": "Tour", + "@language": "fr" + }, + { + "@value": "Torno", + "@language": "es" + }, + { + "@value": "Drehbank", + "@language": "de" + }, + { + "@value": "Lathe", + "@language": "en" + }, + { + "@value": "لاث", + "@language": "ar" + }, + { + "@value": "Lathe", + "@language": "ku" + }, + { + "@value": "Tornio", + "@language": "it" + }, + { + "@value": "Lathe", + "@language": "sw" + }, + { + "@value": "Torno", + "@language": "pt" + }, + { + "@value": "Lathe", + "@language": "oc" + }, + { + "@value": "Лате", + "@language": "ru" + }, + { + "@value": "Lathe", + "@language": "cy" + }, + { + "@value": "ラテ", + "@language": "ja" + }, + { + "@value": "Lámhaigh", + "@language": "ga" + }, + { + "@value": "खराद", + "@language": "hi" + }, + { + "@value": "Lathe", + "@language": "zh" + }, + { + "@value": "Lathe", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#lathe", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#table-saw", + "rdfs:label": [ + { + "@value": "Banc de scie", + "@language": "fr" + }, + { + "@value": "Sierra de mesa", + "@language": "es" + }, + { + "@value": "Tischsäge", + "@language": "de" + }, + { + "@value": "Table Saw", + "@language": "en" + }, + { + "@value": "Table Saw", + "@language": "ar" + }, + { + "@value": "Table Saw", + "@language": "ku" + }, + { + "@value": "Sega da tavolo", + "@language": "it" + }, + { + "@value": "Table Saw", + "@language": "sw" + }, + { + "@value": "Serra de mesa", + "@language": "pt" + }, + { + "@value": "Table Saw", + "@language": "oc" + }, + { + "@value": "Таблица Saw", + "@language": "ru" + }, + { + "@value": "Table Saw", + "@language": "cy" + }, + { + "@value": "テーブルは見ました", + "@language": "ja" + }, + { + "@value": "Tábla Saw", + "@language": "ga" + }, + { + "@value": "टेबल देखा", + "@language": "hi" + }, + { + "@value": "表Saw", + "@language": "zh" + }, + { + "@value": "Table Saw", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#saw", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#mitre-saw", + "rdfs:label": [ + { + "@value": "Scie à onglet", + "@language": "fr" + }, + { + "@value": "Sierra de inglete", + "@language": "es" + }, + { + "@value": "Gehrungssäge", + "@language": "de" + }, + { + "@value": "Mitre Saw", + "@language": "en" + }, + { + "@value": "Mitre Saw", + "@language": "ar" + }, + { + "@value": "Mitre Saw", + "@language": "ku" + }, + { + "@value": "Sega di Mitre", + "@language": "it" + }, + { + "@value": "Mitre Saw", + "@language": "sw" + }, + { + "@value": "Serra de Mitre", + "@language": "pt" + }, + { + "@value": "Mitre Saw", + "@language": "oc" + }, + { + "@value": "Mitre пила", + "@language": "ru" + }, + { + "@value": "Mitre Saw", + "@language": "cy" + }, + { + "@value": "Mitre 見ました", + "@language": "ja" + }, + { + "@value": "cliceáil grianghraf a mhéadú", + "@language": "ga" + }, + { + "@value": "Mitre देखा", + "@language": "hi" + }, + { + "@value": "米特·塞韦斯", + "@language": "zh" + }, + { + "@value": "Mitre Saw", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#saw", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#wire-bending-tool", + "rdfs:label": [ + { + "@value": "Outil de pliage de fil", + "@language": "fr" + }, + { + "@value": "Herramienta de flexión de alambre", + "@language": "es" + }, + { + "@value": "Drahtbiegewerkzeug", + "@language": "de" + }, + { + "@value": "Wire Bending Tool", + "@language": "en" + }, + { + "@value": "Wire Bending Tool", + "@language": "ar" + }, + { + "@value": "Wire Bending Tool", + "@language": "ku" + }, + { + "@value": "Strumento di piegatura del filo", + "@language": "it" + }, + { + "@value": "Wire Bending Tool", + "@language": "sw" + }, + { + "@value": "Ferramenta de dobra de fio", + "@language": "pt" + }, + { + "@value": "Wire Bending Tool", + "@language": "oc" + }, + { + "@value": "Инструмент длягиба проводов", + "@language": "ru" + }, + { + "@value": "Wire Bending Tool", + "@language": "cy" + }, + { + "@value": "ワイヤー曲げ工具", + "@language": "ja" + }, + { + "@value": "Wire Uirlis sheoladh", + "@language": "ga" + }, + { + "@value": "तार झुकने उपकरण", + "@language": "hi" + }, + { + "@value": "F. Wire Bingol", + "@language": "zh" + }, + { + "@value": "Wire Bending Tool", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#wire-bending-tool", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#wood-vice", + "rdfs:label": [ + { + "@value": "Vice du bois", + "@language": "fr" + }, + { + "@value": "Vicio de madera", + "@language": "es" + }, + { + "@value": "Holz Vice", + "@language": "de" + }, + { + "@value": "Wood Vice", + "@language": "en" + }, + { + "@value": "Wood Vice", + "@language": "ar" + }, + { + "@value": "Wood Vice", + "@language": "ku" + }, + { + "@value": "Vice di legno", + "@language": "it" + }, + { + "@value": "Wood Vice", + "@language": "sw" + }, + { + "@value": "Vice de madeira", + "@language": "pt" + }, + { + "@value": "Wood Vice", + "@language": "oc" + }, + { + "@value": "Дерево Вице", + "@language": "ru" + }, + { + "@value": "Wood Vice", + "@language": "cy" + }, + { + "@value": "ウッド・バイス", + "@language": "ja" + }, + { + "@value": "Amharc ar gach eolas", + "@language": "ga" + }, + { + "@value": "वुड वाइस", + "@language": "hi" + }, + { + "@value": "木 员", + "@language": "zh" + }, + { + "@value": "Wood Vice", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#vice", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#metal-vice", + "rdfs:label": [ + { + "@value": "Vice du métal", + "@language": "fr" + }, + { + "@value": "Vicio de metal", + "@language": "es" + }, + { + "@value": "Metallschraube", + "@language": "de" + }, + { + "@value": "Metal Vice", + "@language": "en" + }, + { + "@value": "نائب المعادن", + "@language": "ar" + }, + { + "@value": "Metal Vice", + "@language": "ku" + }, + { + "@value": "Vice metallo", + "@language": "it" + }, + { + "@value": "Metal Vice", + "@language": "sw" + }, + { + "@value": "Vice de Metal", + "@language": "pt" + }, + { + "@value": "Metal Vice", + "@language": "oc" + }, + { + "@value": "Металл Вице", + "@language": "ru" + }, + { + "@value": "Metal Vice", + "@language": "cy" + }, + { + "@value": "メタルバイス", + "@language": "ja" + }, + { + "@value": "An Leas-Cheannfort", + "@language": "ga" + }, + { + "@value": "मेटल वाइस", + "@language": "hi" + }, + { + "@value": "金属副主席", + "@language": "zh" + }, + { + "@value": "Metal Vice", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#vice", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#woodturning-lathe", + "rdfs:label": [ + { + "@value": "Tour de bois", + "@language": "fr" + }, + { + "@value": "Torno de leña", + "@language": "es" + }, + { + "@value": "Holzdrehmaschine", + "@language": "de" + }, + { + "@value": "Woodturning Lathe", + "@language": "en" + }, + { + "@value": "Woodturning Lathe", + "@language": "ar" + }, + { + "@value": "Woodturning Lathe", + "@language": "ku" + }, + { + "@value": "Tornio in legno", + "@language": "it" + }, + { + "@value": "Woodturning Lathe", + "@language": "sw" + }, + { + "@value": "Torno de madeira", + "@language": "pt" + }, + { + "@value": "Woodturning Lathe", + "@language": "oc" + }, + { + "@value": "Деревообращение Lathe", + "@language": "ru" + }, + { + "@value": "Woodturning Lathe", + "@language": "cy" + }, + { + "@value": "木製の回転旋盤", + "@language": "ja" + }, + { + "@value": "Láimhseálann adhmaid", + "@language": "ga" + }, + { + "@value": "Woodturning खराद", + "@language": "hi" + }, + { + "@value": "B. 木工", + "@language": "zh" + }, + { + "@value": "Woodturning Lathe", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#lathe", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#multimeter", + "rdfs:label": [ + { + "@value": "Multimètre", + "@language": "fr" + }, + { + "@value": "Multímetro", + "@language": "es" + }, + { + "@value": "Multimeter", + "@language": "de" + }, + { + "@value": "Multimeter", + "@language": "en" + }, + { + "@value": "متعددة", + "@language": "ar" + }, + { + "@value": "Multimeter", + "@language": "ku" + }, + { + "@value": "Multimetro", + "@language": "it" + }, + { + "@value": "Multimeter", + "@language": "sw" + }, + { + "@value": "Multímetro", + "@language": "pt" + }, + { + "@value": "Multimeter", + "@language": "oc" + }, + { + "@value": "Мультиметр", + "@language": "ru" + }, + { + "@value": "Multimeter", + "@language": "cy" + }, + { + "@value": "マルチメーター", + "@language": "ja" + }, + { + "@value": "Uirlisí ilchuspóireacha", + "@language": "ga" + }, + { + "@value": "मल्टीमीटर", + "@language": "hi" + }, + { + "@value": "2. 多参数", + "@language": "zh" + }, + { + "@value": "Multimeter", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#electrical", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#bench-power-supply", + "rdfs:label": [ + { + "@value": "Alimentation électrique", + "@language": "fr" + }, + { + "@value": "Fuente de alimentación de banco", + "@language": "es" + }, + { + "@value": "Bankstromversorgung", + "@language": "de" + }, + { + "@value": "Bench Power Supply", + "@language": "en" + }, + { + "@value": "هيئة الإمداد", + "@language": "ar" + }, + { + "@value": "Bench Power Supply", + "@language": "ku" + }, + { + "@value": "Alimentazione elettrica del banco", + "@language": "it" + }, + { + "@value": "Bench Power Supply", + "@language": "sw" + }, + { + "@value": "Fonte de alimentação do banco", + "@language": "pt" + }, + { + "@value": "Bench Power Supply", + "@language": "oc" + }, + { + "@value": "Бенч Power Supply", + "@language": "ru" + }, + { + "@value": "Bench Power Supply", + "@language": "cy" + }, + { + "@value": "ベンチの電源", + "@language": "ja" + }, + { + "@value": "Soláthar Cumhachta Binse", + "@language": "ga" + }, + { + "@value": "बेंच बिजली की आपूर्ति", + "@language": "hi" + }, + { + "@value": "B. 电力供应", + "@language": "zh" + }, + { + "@value": "Bench Power Supply", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#electrical", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#capacitance-meter", + "rdfs:label": [ + { + "@value": "Compteur de capacités", + "@language": "fr" + }, + { + "@value": "Medidor de capacitancia", + "@language": "es" + }, + { + "@value": "Kapazitätsmesser", + "@language": "de" + }, + { + "@value": "Capacitance Meter", + "@language": "en" + }, + { + "@value": "Meter", + "@language": "ar" + }, + { + "@value": "Capacitance Meter", + "@language": "ku" + }, + { + "@value": "Misuratore di capacità", + "@language": "it" + }, + { + "@value": "Capacitance Meter", + "@language": "sw" + }, + { + "@value": "Medidor de capacidade", + "@language": "pt" + }, + { + "@value": "Capacitance Meter", + "@language": "oc" + }, + { + "@value": "Capacitance метр", + "@language": "ru" + }, + { + "@value": "Capacitance Meter", + "@language": "cy" + }, + { + "@value": "容量のメートル", + "@language": "ja" + }, + { + "@value": "Méadar Capacitance", + "@language": "ga" + }, + { + "@value": "समाई मीटर", + "@language": "hi" + }, + { + "@value": "资格金属", + "@language": "zh" + }, + { + "@value": "Capacitance Meter", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#electrical", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#oscilloscope", + "rdfs:label": [ + { + "@value": "Oscilloscope", + "@language": "fr" + }, + { + "@value": "Osciloscopio", + "@language": "es" + }, + { + "@value": "Oszilloskop", + "@language": "de" + }, + { + "@value": "Oscilloscope", + "@language": "en" + }, + { + "@value": "Oscilloscope", + "@language": "ar" + }, + { + "@value": "Oscilloscope", + "@language": "ku" + }, + { + "@value": "Oscilloscopio", + "@language": "it" + }, + { + "@value": "Oscilloscope", + "@language": "sw" + }, + { + "@value": "Osciloscópio", + "@language": "pt" + }, + { + "@value": "Oscilloscope", + "@language": "oc" + }, + { + "@value": "Осциллоскоп", + "@language": "ru" + }, + { + "@value": "Oscilloscope", + "@language": "cy" + }, + { + "@value": "オシロスコープ", + "@language": "ja" + }, + { + "@value": "Seirbhís do Chustaiméirí", + "@language": "ga" + }, + { + "@value": "ऑस्किलोस्कोप", + "@language": "hi" + }, + { + "@value": "奥斯卡蒂·米格尔", + "@language": "zh" + }, + { + "@value": "Oscilloscope", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#electrical", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#oscilloscope-probes", + "rdfs:label": [ + { + "@value": "Sondes d'oscilloscope", + "@language": "fr" + }, + { + "@value": "Sondas de osciloscopio", + "@language": "es" + }, + { + "@value": "Oszilloskopsonden", + "@language": "de" + }, + { + "@value": "Oscilloscope Probes", + "@language": "en" + }, + { + "@value": "Oscilloscope Probes", + "@language": "ar" + }, + { + "@value": "Oscilloscope Probes", + "@language": "ku" + }, + { + "@value": "Sonde di Oscilloscopio", + "@language": "it" + }, + { + "@value": "Oscilloscope Probes", + "@language": "sw" + }, + { + "@value": "Sondas de osciloscópio", + "@language": "pt" + }, + { + "@value": "Oscilloscope Probes", + "@language": "oc" + }, + { + "@value": "Осциллоскоп Probes", + "@language": "ru" + }, + { + "@value": "Oscilloscope Probes", + "@language": "cy" + }, + { + "@value": "Oscilloscopeの調査", + "@language": "ja" + }, + { + "@value": "Seirbhís do Chustaiméirí", + "@language": "ga" + }, + { + "@value": "Oscilloscope Probes", + "@language": "hi" + }, + { + "@value": "奥斯卡洛斯·普贝", + "@language": "zh" + }, + { + "@value": "Oscilloscope Probes", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#electrical", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#signal-generator", + "rdfs:label": [ + { + "@value": "Générateur de signal", + "@language": "fr" + }, + { + "@value": "Generador de señales", + "@language": "es" + }, + { + "@value": "Signalgenerator", + "@language": "de" + }, + { + "@value": "Signal Generator", + "@language": "en" + }, + { + "@value": "مولد الإشارة", + "@language": "ar" + }, + { + "@value": "Signal Generator", + "@language": "ku" + }, + { + "@value": "Generatore di segnale", + "@language": "it" + }, + { + "@value": "Signal Generator", + "@language": "sw" + }, + { + "@value": "Gerador de sinal", + "@language": "pt" + }, + { + "@value": "Signal Generator", + "@language": "oc" + }, + { + "@value": "Генератор сигналов", + "@language": "ru" + }, + { + "@value": "Signal Generator", + "@language": "cy" + }, + { + "@value": "信号の発電機", + "@language": "ja" + }, + { + "@value": "Gineadóir Comhartha", + "@language": "ga" + }, + { + "@value": "सिग्नल जेनरेटर", + "@language": "hi" + }, + { + "@value": "签字国", + "@language": "zh" + }, + { + "@value": "Signal Generator", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#signal-generator", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#mains-transformer", + "rdfs:label": [ + { + "@value": "Transformateur principal", + "@language": "fr" + }, + { + "@value": "Transformador de red", + "@language": "es" + }, + { + "@value": "Netzwandler", + "@language": "de" + }, + { + "@value": "Mains Transformer", + "@language": "en" + }, + { + "@value": "Mains Transformer", + "@language": "ar" + }, + { + "@value": "Mains Transformer", + "@language": "ku" + }, + { + "@value": "Trasformatore principale", + "@language": "it" + }, + { + "@value": "Mains Transformer", + "@language": "sw" + }, + { + "@value": "Transformador de Principals", + "@language": "pt" + }, + { + "@value": "Mains Transformer", + "@language": "oc" + }, + { + "@value": "Основы Transformer", + "@language": "ru" + }, + { + "@value": "Mains Transformer", + "@language": "cy" + }, + { + "@value": "主要な変圧器", + "@language": "ja" + }, + { + "@value": "Cad iad na buntáistí a bhaineann le quercetin...", + "@language": "ga" + }, + { + "@value": "मेन्स ट्रांसफार्मर", + "@language": "hi" + }, + { + "@value": "主要翻译", + "@language": "zh" + }, + { + "@value": "Mains Transformer", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#electrical", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#hot-air-gun", + "rdfs:label": [ + { + "@value": "Pistolet à air chaud", + "@language": "fr" + }, + { + "@value": "Pistola de aire caliente", + "@language": "es" + }, + { + "@value": "Heiße Luft Pistole", + "@language": "de" + }, + { + "@value": "Hot Air Gun", + "@language": "en" + }, + { + "@value": "مدفع هوائي", + "@language": "ar" + }, + { + "@value": "Hot Air Gun", + "@language": "ku" + }, + { + "@value": "Pistola ad aria calda", + "@language": "it" + }, + { + "@value": "Hot Air Gun", + "@language": "sw" + }, + { + "@value": "Arma de ar quente", + "@language": "pt" + }, + { + "@value": "Hot Air Gun", + "@language": "oc" + }, + { + "@value": "Горячий воздушный пистолет", + "@language": "ru" + }, + { + "@value": "Hot Air Gun", + "@language": "cy" + }, + { + "@value": "ホットエアガン", + "@language": "ja" + }, + { + "@value": "Te Aeir Gunna", + "@language": "ga" + }, + { + "@value": "हॉट एयर गन", + "@language": "hi" + }, + { + "@value": "Hot Air Gun", + "@language": "zh" + }, + { + "@value": "Hot Air Gun", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#heater", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#bench-pillar-drill", + "rdfs:label": [ + { + "@value": "Percaire de banc", + "@language": "fr" + }, + { + "@value": "Taladro de pilar de banco", + "@language": "es" + }, + { + "@value": "Bank-Säulenbohrer", + "@language": "de" + }, + { + "@value": "Bench Pillar Drill", + "@language": "en" + }, + { + "@value": "Bench Pillar Drill", + "@language": "ar" + }, + { + "@value": "Bench Pillar Drill", + "@language": "ku" + }, + { + "@value": "Trapano del pilastro del banco", + "@language": "it" + }, + { + "@value": "Bench Pillar Drill", + "@language": "sw" + }, + { + "@value": "Broca de pilar de bancada", + "@language": "pt" + }, + { + "@value": "Bench Pillar Drill", + "@language": "oc" + }, + { + "@value": "Бенч Pillar дрель", + "@language": "ru" + }, + { + "@value": "Bench Pillar Drill", + "@language": "cy" + }, + { + "@value": "ベンチピラードリル", + "@language": "ja" + }, + { + "@value": "Druileáil Cholún Bench", + "@language": "ga" + }, + { + "@value": "बेंच स्तंभ ड्रिल", + "@language": "hi" + }, + { + "@value": "B. 本金·普里尔", + "@language": "zh" + }, + { + "@value": "Bench Pillar Drill", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#drill", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#bench-magnifier-lamp", + "rdfs:label": [ + { + "@value": "Lampe de loupe de banc", + "@language": "fr" + }, + { + "@value": "Lámpara de la lupa del banco", + "@language": "es" + }, + { + "@value": "Banklatch-Lampe", + "@language": "de" + }, + { + "@value": "Bench Magnifier Lamp", + "@language": "en" + }, + { + "@value": "جهاز التبريد Lamp", + "@language": "ar" + }, + { + "@value": "Bench Magnifier Lamp", + "@language": "ku" + }, + { + "@value": "Magnifier del banco Lampada", + "@language": "it" + }, + { + "@value": "Bench Magnifier Lamp", + "@language": "sw" + }, + { + "@value": "Bench Magnifier Lâmpada", + "@language": "pt" + }, + { + "@value": "Bench Magnifier Lamp", + "@language": "oc" + }, + { + "@value": "Бенч Magnifier Лампа", + "@language": "ru" + }, + { + "@value": "Bench Magnifier Lamp", + "@language": "cy" + }, + { + "@value": "ベンチの拡大鏡 ランプ", + "@language": "ja" + }, + { + "@value": "Bench Magnifier Déan teagmháil linn", + "@language": "ga" + }, + { + "@value": "बेंच मैग्नीफायर लैंप", + "@language": "hi" + }, + { + "@value": "本金·马尼菲尔 小册子", + "@language": "zh" + }, + { + "@value": "Bench Magnifier Lamp", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#bench-magnifier-lamp", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#electronic-hotplate", + "rdfs:label": [ + { + "@value": "Plaque de feu électronique", + "@language": "fr" + }, + { + "@value": "Placa electrónica", + "@language": "es" + }, + { + "@value": "Elektronische Kochplatte", + "@language": "de" + }, + { + "@value": "Electronic Hotplate", + "@language": "en" + }, + { + "@value": "جهاز هوتبل إلكتروني", + "@language": "ar" + }, + { + "@value": "Electronic Hotplate", + "@language": "ku" + }, + { + "@value": "Hotplate elettronico", + "@language": "it" + }, + { + "@value": "Electronic Hotplate", + "@language": "sw" + }, + { + "@value": "Hotplate eletrônico", + "@language": "pt" + }, + { + "@value": "Electronic Hotplate", + "@language": "oc" + }, + { + "@value": "Электронная плита", + "@language": "ru" + }, + { + "@value": "Electronic Hotplate", + "@language": "cy" + }, + { + "@value": "電子ホットプレート", + "@language": "ja" + }, + { + "@value": "Hotplate Leictreonach", + "@language": "ga" + }, + { + "@value": "इलेक्ट्रॉनिक हॉटप्लेट", + "@language": "hi" + }, + { + "@value": "电子电话会议", + "@language": "zh" + }, + { + "@value": "Electronic Hotplate", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#heater", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#microscope", + "rdfs:label": [ + { + "@value": "Microscope", + "@language": "fr" + }, + { + "@value": "Microscopio", + "@language": "es" + }, + { + "@value": "Mikroskop", + "@language": "de" + }, + { + "@value": "Microscope", + "@language": "en" + }, + { + "@value": "Microscope", + "@language": "ar" + }, + { + "@value": "Microscope", + "@language": "ku" + }, + { + "@value": "Microscopio", + "@language": "it" + }, + { + "@value": "Microscope", + "@language": "sw" + }, + { + "@value": "Microscópio", + "@language": "pt" + }, + { + "@value": "Microscope", + "@language": "oc" + }, + { + "@value": "Микроскоп", + "@language": "ru" + }, + { + "@value": "Microscope", + "@language": "cy" + }, + { + "@value": "マイクロスコープ", + "@language": "ja" + }, + { + "@value": "Micreascópacha", + "@language": "ga" + }, + { + "@value": "माइक्रोस्कोप", + "@language": "hi" + }, + { + "@value": "微量素", + "@language": "zh" + }, + { + "@value": "Microscope", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#microscope", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#soldering-iron", + "rdfs:label": [ + { + "@value": "Fer à souder", + "@language": "fr" + }, + { + "@value": "Soldador", + "@language": "es" + }, + { + "@value": "Lötkolben", + "@language": "de" + }, + { + "@value": "Soldering Iron", + "@language": "en" + }, + { + "@value": "حل الحديد", + "@language": "ar" + }, + { + "@value": "Soldering Iron", + "@language": "ku" + }, + { + "@value": "Ferro da stiro", + "@language": "it" + }, + { + "@value": "Soldering Iron", + "@language": "sw" + }, + { + "@value": "Ferro de Solda", + "@language": "pt" + }, + { + "@value": "Soldering Iron", + "@language": "oc" + }, + { + "@value": "Паяльное железо", + "@language": "ru" + }, + { + "@value": "Soldering Iron", + "@language": "cy" + }, + { + "@value": "はんだ付けする鉄", + "@language": "ja" + }, + { + "@value": "Iarann Soldering", + "@language": "ga" + }, + { + "@value": "सोल्डरिंग आयरन", + "@language": "hi" + }, + { + "@value": "溶剂", + "@language": "zh" + }, + { + "@value": "Soldering Iron", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#soldering", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#solder-reflow-oven", + "rdfs:label": [ + { + "@value": "Four de refusion à souder", + "@language": "fr" + }, + { + "@value": "Horno de reflujo de soldadura", + "@language": "es" + }, + { + "@value": "Lot-Reflow-Ofen", + "@language": "de" + }, + { + "@value": "Solder Reflow Oven", + "@language": "en" + }, + { + "@value": "منظمة سولدر", + "@language": "ar" + }, + { + "@value": "Solder Reflow Oven", + "@language": "ku" + }, + { + "@value": "Forno di riflusso di sambuco", + "@language": "it" + }, + { + "@value": "Solder Reflow Oven", + "@language": "sw" + }, + { + "@value": "Forno de fluxo de solda", + "@language": "pt" + }, + { + "@value": "Solder Reflow Oven", + "@language": "oc" + }, + { + "@value": "Солдат переполнения печь", + "@language": "ru" + }, + { + "@value": "Solder Reflow Oven", + "@language": "cy" + }, + { + "@value": "はんだの退潮のオーブン", + "@language": "ja" + }, + { + "@value": "Sreabhadh Solder Oven", + "@language": "ga" + }, + { + "@value": "सोल्डर रिफ्लो ओवन", + "@language": "hi" + }, + { + "@value": "溶剂后流", + "@language": "zh" + }, + { + "@value": "Solder Reflow Oven", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#heater", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#spot-welder", + "rdfs:label": [ + { + "@value": "Soudeur tache", + "@language": "fr" + }, + { + "@value": "Soldadura por puntos", + "@language": "es" + }, + { + "@value": "Punktschweißgerät", + "@language": "de" + }, + { + "@value": "Spot Welder", + "@language": "en" + }, + { + "@value": "Spot Welder", + "@language": "ar" + }, + { + "@value": "Spot Welder", + "@language": "ku" + }, + { + "@value": "Spot Welder", + "@language": "it" + }, + { + "@value": "Spot Welder", + "@language": "sw" + }, + { + "@value": "Soldados do local", + "@language": "pt" + }, + { + "@value": "Spot Welder", + "@language": "oc" + }, + { + "@value": "Спот Welder", + "@language": "ru" + }, + { + "@value": "Spot Welder", + "@language": "cy" + }, + { + "@value": "スポット溶接機", + "@language": "ja" + }, + { + "@value": "Sútha talún", + "@language": "ga" + }, + { + "@value": "स्पॉट वेल्डर", + "@language": "hi" + }, + { + "@value": "Spot Welder", + "@language": "zh" + }, + { + "@value": "Spot Welder", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#welding", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#flat-bed-pen-plotter", + "rdfs:label": [ + { + "@value": "Traceur de stylo plat", + "@language": "fr" + }, + { + "@value": "Pluma de lecho plana", + "@language": "es" + }, + { + "@value": "Flachbett-Stift-Plotter", + "@language": "de" + }, + { + "@value": "Flat-Bed Pen Plotter", + "@language": "en" + }, + { + "@value": "Flat-Bed Pen Plotter", + "@language": "ar" + }, + { + "@value": "Flat-Bed Pen Plotter", + "@language": "ku" + }, + { + "@value": "Plotter della penna piatta", + "@language": "it" + }, + { + "@value": "Flat-Bed Pen Plotter", + "@language": "sw" + }, + { + "@value": "Plotter de caneta plana", + "@language": "pt" + }, + { + "@value": "Flat-Bed Pen Plotter", + "@language": "oc" + }, + { + "@value": "Плоский пилот", + "@language": "ru" + }, + { + "@value": "Flat-Bed Pen Plotter", + "@language": "cy" + }, + { + "@value": "フラットベッドペンポッター", + "@language": "ja" + }, + { + "@value": "Maol-Bed Pltter Peann", + "@language": "ga" + }, + { + "@value": "फ्लैट-बेड पेन प्लॉटर", + "@language": "hi" + }, + { + "@value": "Flat-Bed Pen Plotter", + "@language": "zh" + }, + { + "@value": "Flat-Bed Pen Plotter", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#flat-bed-pen-plotter", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#printer", + "rdfs:label": [ + { + "@value": "Imprimante", + "@language": "fr" + }, + { + "@value": "Impresora", + "@language": "es" + }, + { + "@value": "Druckerin", + "@language": "de" + }, + { + "@value": "Printer", + "@language": "en" + }, + { + "@value": "Printer", + "@language": "ar" + }, + { + "@value": "Printer", + "@language": "ku" + }, + { + "@value": "Stampante", + "@language": "it" + }, + { + "@value": "Printer", + "@language": "sw" + }, + { + "@value": "Impressão", + "@language": "pt" + }, + { + "@value": "Printer", + "@language": "oc" + }, + { + "@value": "принтер", + "@language": "ru" + }, + { + "@value": "Printer", + "@language": "cy" + }, + { + "@value": "プリンター", + "@language": "ja" + }, + { + "@value": "Déan Teagmháil Linn", + "@language": "ga" + }, + { + "@value": "प्रिंटर", + "@language": "hi" + }, + { + "@value": "Printer", + "@language": "zh" + }, + { + "@value": "Printer", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#printer", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#laminator", + "rdfs:label": [ + { + "@value": "Laminateur", + "@language": "fr" + }, + { + "@value": "Laminador", + "@language": "es" + }, + { + "@value": "Laminator", + "@language": "de" + }, + { + "@value": "Laminator", + "@language": "en" + }, + { + "@value": "الملاح", + "@language": "ar" + }, + { + "@value": "Laminator", + "@language": "ku" + }, + { + "@value": "Laminatore", + "@language": "it" + }, + { + "@value": "Laminator", + "@language": "sw" + }, + { + "@value": "Laminador", + "@language": "pt" + }, + { + "@value": "Laminator", + "@language": "oc" + }, + { + "@value": "Ламинатор", + "@language": "ru" + }, + { + "@value": "Laminator", + "@language": "cy" + }, + { + "@value": "ラミネート", + "@language": "ja" + }, + { + "@value": "Lámhleabhair", + "@language": "ga" + }, + { + "@value": "लामिनेटर", + "@language": "hi" + }, + { + "@value": "提名", + "@language": "zh" + }, + { + "@value": "Laminator", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#laminator", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#thermal-camera", + "rdfs:label": [ + { + "@value": "Caméra thermique", + "@language": "fr" + }, + { + "@value": "Cámara térmica", + "@language": "es" + }, + { + "@value": "Wärmebildkamera", + "@language": "de" + }, + { + "@value": "Thermal Camera", + "@language": "en" + }, + { + "@value": "الكاميرا الحرارية", + "@language": "ar" + }, + { + "@value": "Thermal Camera", + "@language": "ku" + }, + { + "@value": "Macchina fotografica termica", + "@language": "it" + }, + { + "@value": "Thermal Camera", + "@language": "sw" + }, + { + "@value": "Máquina térmica", + "@language": "pt" + }, + { + "@value": "Thermal Camera", + "@language": "oc" + }, + { + "@value": "Термальная камера", + "@language": "ru" + }, + { + "@value": "Thermal Camera", + "@language": "cy" + }, + { + "@value": "熱カメラ", + "@language": "ja" + }, + { + "@value": "Ceamara Teirmeach", + "@language": "ga" + }, + { + "@value": "थर्मल कैमरा", + "@language": "hi" + }, + { + "@value": "摩尔·卡拉", + "@language": "zh" + }, + { + "@value": "Thermal Camera", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#camera", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#glue-gun", + "rdfs:label": [ + { + "@value": "Pistolet à colle", + "@language": "fr" + }, + { + "@value": "Pistola de pegamento", + "@language": "es" + }, + { + "@value": "Klebepistole", + "@language": "de" + }, + { + "@value": "Glue Gun", + "@language": "en" + }, + { + "@value": "Glue Gun", + "@language": "ar" + }, + { + "@value": "Glue Gun", + "@language": "ku" + }, + { + "@value": "Pistola di colla", + "@language": "it" + }, + { + "@value": "Glue Gun", + "@language": "sw" + }, + { + "@value": "Arma de cola", + "@language": "pt" + }, + { + "@value": "Glue Gun", + "@language": "oc" + }, + { + "@value": "Клей пистолет", + "@language": "ru" + }, + { + "@value": "Glue Gun", + "@language": "cy" + }, + { + "@value": "グルーガン", + "@language": "ja" + }, + { + "@value": "Gliú Gunna", + "@language": "ga" + }, + { + "@value": "गोंद गन", + "@language": "hi" + }, + { + "@value": "Glue Gun", + "@language": "zh" + }, + { + "@value": "Glue Gun", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#glue-gun", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#hot-air-gun", + "rdfs:label": [ + { + "@value": "Pistolet à air chaud", + "@language": "fr" + }, + { + "@value": "Pistola de aire caliente", + "@language": "es" + }, + { + "@value": "Heiße Luft Pistole", + "@language": "de" + }, + { + "@value": "Hot Air Gun", + "@language": "en" + }, + { + "@value": "مدفع هوائي", + "@language": "ar" + }, + { + "@value": "Hot Air Gun", + "@language": "ku" + }, + { + "@value": "Pistola ad aria calda", + "@language": "it" + }, + { + "@value": "Hot Air Gun", + "@language": "sw" + }, + { + "@value": "Arma de ar quente", + "@language": "pt" + }, + { + "@value": "Hot Air Gun", + "@language": "oc" + }, + { + "@value": "Горячий воздушный пистолет", + "@language": "ru" + }, + { + "@value": "Hot Air Gun", + "@language": "cy" + }, + { + "@value": "ホットエアガン", + "@language": "ja" + }, + { + "@value": "Te Aeir Gunna", + "@language": "ga" + }, + { + "@value": "हॉट एयर गन", + "@language": "hi" + }, + { + "@value": "Hot Air Gun", + "@language": "zh" + }, + { + "@value": "Hot Air Gun", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#hot-air-gun", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#label-printer", + "rdfs:label": [ + { + "@value": "Imprimante d'étiquettes", + "@language": "fr" + }, + { + "@value": "Impresora de etiquetas", + "@language": "es" + }, + { + "@value": "Etikettendrucker", + "@language": "de" + }, + { + "@value": "Label Printer", + "@language": "en" + }, + { + "@value": "Label Printer", + "@language": "ar" + }, + { + "@value": "Label Printer", + "@language": "ku" + }, + { + "@value": "Stampante di etichette", + "@language": "it" + }, + { + "@value": "Label Printer", + "@language": "sw" + }, + { + "@value": "Impressão de etiquetas", + "@language": "pt" + }, + { + "@value": "Label Printer", + "@language": "oc" + }, + { + "@value": "Принтер этикетки", + "@language": "ru" + }, + { + "@value": "Label Printer", + "@language": "cy" + }, + { + "@value": "ラベルプリンター", + "@language": "ja" + }, + { + "@value": "Priontáil Lipéad", + "@language": "ga" + }, + { + "@value": "लेबल प्रिंटर", + "@language": "hi" + }, + { + "@value": "Label Printer", + "@language": "zh" + }, + { + "@value": "Label Printer", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#label-printer", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#sewing-machine", + "rdfs:label": [ + { + "@value": "Machine à coudre", + "@language": "fr" + }, + { + "@value": "Máquina de coser", + "@language": "es" + }, + { + "@value": "Nähmaschine", + "@language": "de" + }, + { + "@value": "Sewing Machine", + "@language": "en" + }, + { + "@value": "Sewing Machine", + "@language": "ar" + }, + { + "@value": "Sewing Machine", + "@language": "ku" + }, + { + "@value": "Macchina per cucire", + "@language": "it" + }, + { + "@value": "Sewing Machine", + "@language": "sw" + }, + { + "@value": "Máquina de costura", + "@language": "pt" + }, + { + "@value": "Sewing Machine", + "@language": "oc" + }, + { + "@value": "Швейная машина", + "@language": "ru" + }, + { + "@value": "Sewing Machine", + "@language": "cy" + }, + { + "@value": "縫う機械", + "@language": "ja" + }, + { + "@value": "Meaisín Swing", + "@language": "ga" + }, + { + "@value": "सिलाई मशीन", + "@language": "hi" + }, + { + "@value": "塞莱德·梅纳", + "@language": "zh" + }, + { + "@value": "Sewing Machine", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#sewing-machine", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#laptop", + "rdfs:label": [ + { + "@value": "Portable", + "@language": "fr" + }, + { + "@value": "Computadora portátil", + "@language": "es" + }, + { + "@value": "Laptop", + "@language": "de" + }, + { + "@value": "Laptop", + "@language": "en" + }, + { + "@value": "لابوتو", + "@language": "ar" + }, + { + "@value": "Laptop", + "@language": "ku" + }, + { + "@value": "Computer portatile", + "@language": "it" + }, + { + "@value": "Laptop", + "@language": "sw" + }, + { + "@value": "Computador portátil", + "@language": "pt" + }, + { + "@value": "Laptop", + "@language": "oc" + }, + { + "@value": "Ноутбук", + "@language": "ru" + }, + { + "@value": "Laptop", + "@language": "cy" + }, + { + "@value": "ノートパソコン", + "@language": "ja" + }, + { + "@value": "Laptop", + "@language": "ga" + }, + { + "@value": "लैपटॉप", + "@language": "hi" + }, + { + "@value": "招待费", + "@language": "zh" + }, + { + "@value": "Laptop", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#computing", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#desktop-computer", + "rdfs:label": [ + { + "@value": "Ordinateur de bureau", + "@language": "fr" + }, + { + "@value": "Computadora de escritorio", + "@language": "es" + }, + { + "@value": "Desktop-Computer", + "@language": "de" + }, + { + "@value": "Desktop Computer", + "@language": "en" + }, + { + "@value": "حاسوب مكتبي", + "@language": "ar" + }, + { + "@value": "Desktop Computer", + "@language": "ku" + }, + { + "@value": "Computer desktop", + "@language": "it" + }, + { + "@value": "Desktop Computer", + "@language": "sw" + }, + { + "@value": "Computador de secretária", + "@language": "pt" + }, + { + "@value": "Desktop Computer", + "@language": "oc" + }, + { + "@value": "Компьютер", + "@language": "ru" + }, + { + "@value": "Desktop Computer", + "@language": "cy" + }, + { + "@value": "デスクトップコンピュータ", + "@language": "ja" + }, + { + "@value": "Deisceabal Ríomhaireachta", + "@language": "ga" + }, + { + "@value": "डेस्कटॉप कंप्यूटर", + "@language": "hi" + }, + { + "@value": "计算机", + "@language": "zh" + }, + { + "@value": "Desktop Computer", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#computing", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#computer-monitor", + "rdfs:label": [ + { + "@value": "Moniteur d'ordinateur", + "@language": "fr" + }, + { + "@value": "Monitor de computadora", + "@language": "es" + }, + { + "@value": "Computerbildschirm", + "@language": "de" + }, + { + "@value": "Computer Monitor", + "@language": "en" + }, + { + "@value": "مرصد الحاسوب", + "@language": "ar" + }, + { + "@value": "Computer Monitor", + "@language": "ku" + }, + { + "@value": "Monitoraggio del computer", + "@language": "it" + }, + { + "@value": "Computer Monitor", + "@language": "sw" + }, + { + "@value": "Monitor de computador", + "@language": "pt" + }, + { + "@value": "Computer Monitor", + "@language": "oc" + }, + { + "@value": "Компьютерный монитор", + "@language": "ru" + }, + { + "@value": "Computer Monitor", + "@language": "cy" + }, + { + "@value": "コンピュータ モニター", + "@language": "ja" + }, + { + "@value": "Monatóireacht a dhéanamh ar ríomhaire", + "@language": "ga" + }, + { + "@value": "कंप्यूटर मॉनिटर", + "@language": "hi" + }, + { + "@value": "计算机监测", + "@language": "zh" + }, + { + "@value": "Computer Monitor", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#computing", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#computer-mouse", + "rdfs:label": [ + { + "@value": "Souris d'ordinateur", + "@language": "fr" + }, + { + "@value": "Ratón de computadora", + "@language": "es" + }, + { + "@value": "Computermaus", + "@language": "de" + }, + { + "@value": "Computer Mouse", + "@language": "en" + }, + { + "@value": "استخدام الحاسوب", + "@language": "ar" + }, + { + "@value": "Computer Mouse", + "@language": "ku" + }, + { + "@value": "Computer Mouse", + "@language": "it" + }, + { + "@value": "Computer Mouse", + "@language": "sw" + }, + { + "@value": "Mouse de computador", + "@language": "pt" + }, + { + "@value": "Computer Mouse", + "@language": "oc" + }, + { + "@value": "Компьютерная мышь", + "@language": "ru" + }, + { + "@value": "Computer Mouse", + "@language": "cy" + }, + { + "@value": "コンピュータマウス", + "@language": "ja" + }, + { + "@value": "Ríomhaire Mouse", + "@language": "ga" + }, + { + "@value": "कंप्यूटर माउस", + "@language": "hi" + }, + { + "@value": "计算机 Mouse", + "@language": "zh" + }, + { + "@value": "Computer Mouse", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#computing", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#computer-trackball", + "rdfs:label": [ + { + "@value": "Computer Shueball", + "@language": "fr" + }, + { + "@value": "Trackball informático", + "@language": "es" + }, + { + "@value": "Computer Trackball", + "@language": "de" + }, + { + "@value": "Computer Trackball", + "@language": "en" + }, + { + "@value": "ملعب كرة السلة", + "@language": "ar" + }, + { + "@value": "Computer Trackball", + "@language": "ku" + }, + { + "@value": "Computer Trackball", + "@language": "it" + }, + { + "@value": "Computer Trackball", + "@language": "sw" + }, + { + "@value": "Rastreador de computador", + "@language": "pt" + }, + { + "@value": "Computer Trackball", + "@language": "oc" + }, + { + "@value": "Компьютер Trackball", + "@language": "ru" + }, + { + "@value": "Computer Trackball", + "@language": "cy" + }, + { + "@value": "コンピュータトラックボール", + "@language": "ja" + }, + { + "@value": "Rianú Ríomhaire", + "@language": "ga" + }, + { + "@value": "कंप्यूटर ट्रैकबॉल", + "@language": "hi" + }, + { + "@value": "计算机轨道", + "@language": "zh" + }, + { + "@value": "Computer Trackball", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#computing", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#computer-drawing-tablet", + "rdfs:label": [ + { + "@value": "Tablette de dessin informatique", + "@language": "fr" + }, + { + "@value": "Tableta de dibujo de computadora", + "@language": "es" + }, + { + "@value": "Computerzeichnungsablette", + "@language": "de" + }, + { + "@value": "Computer Drawing Tablet", + "@language": "en" + }, + { + "@value": "جدول رسم الخرائط الحاسوبية", + "@language": "ar" + }, + { + "@value": "Computer Drawing Tablet", + "@language": "ku" + }, + { + "@value": "Tavolino di disegno del computer", + "@language": "it" + }, + { + "@value": "Computer Drawing Tablet", + "@language": "sw" + }, + { + "@value": "Tablet de desenho de computador", + "@language": "pt" + }, + { + "@value": "Computer Drawing Tablet", + "@language": "oc" + }, + { + "@value": "Компьютерный ящик Tablet", + "@language": "ru" + }, + { + "@value": "Computer Drawing Tablet", + "@language": "cy" + }, + { + "@value": "コンピュータのデッサンのタブレット", + "@language": "ja" + }, + { + "@value": "Líníocht Ríomhaire Tablet", + "@language": "ga" + }, + { + "@value": "कंप्यूटर ड्राइंग टैबलेट", + "@language": "hi" + }, + { + "@value": "计算机配量", + "@language": "zh" + }, + { + "@value": "Computer Drawing Tablet", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#computer-drawing", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#computer-webcam", + "rdfs:label": [ + { + "@value": "Webcam", + "@language": "fr" + }, + { + "@value": "Webcam", + "@language": "es" + }, + { + "@value": "Computer Webcam", + "@language": "de" + }, + { + "@value": "Computer Webcam", + "@language": "en" + }, + { + "@value": "حاسوب ويبكام", + "@language": "ar" + }, + { + "@value": "Computer Webcam", + "@language": "ku" + }, + { + "@value": "Computer Webcam", + "@language": "it" + }, + { + "@value": "Computer Webcam", + "@language": "sw" + }, + { + "@value": "Webcam de computador", + "@language": "pt" + }, + { + "@value": "Computer Webcam", + "@language": "oc" + }, + { + "@value": "Компьютер Webcam", + "@language": "ru" + }, + { + "@value": "Computer Webcam", + "@language": "cy" + }, + { + "@value": "コンピュータウェブカム", + "@language": "ja" + }, + { + "@value": "Ceamara gréasáin", + "@language": "ga" + }, + { + "@value": "कंप्यूटर वेबकैम", + "@language": "hi" + }, + { + "@value": "计算机网", + "@language": "zh" + }, + { + "@value": "Computer Webcam", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#computing", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#computer-microphone", + "rdfs:label": [ + { + "@value": "Microphone informatique", + "@language": "fr" + }, + { + "@value": "Micrófono de computadora", + "@language": "es" + }, + { + "@value": "Computermikrofon", + "@language": "de" + }, + { + "@value": "Computer Microphone", + "@language": "en" + }, + { + "@value": "ميكروفون حاسوبي", + "@language": "ar" + }, + { + "@value": "Computer Microphone", + "@language": "ku" + }, + { + "@value": "Microfono del computer", + "@language": "it" + }, + { + "@value": "Computer Microphone", + "@language": "sw" + }, + { + "@value": "Microfone de computador", + "@language": "pt" + }, + { + "@value": "Computer Microphone", + "@language": "oc" + }, + { + "@value": "Компьютерный микрофон", + "@language": "ru" + }, + { + "@value": "Computer Microphone", + "@language": "cy" + }, + { + "@value": "コンピュータマイク", + "@language": "ja" + }, + { + "@value": "Micreafón Ríomhaire", + "@language": "ga" + }, + { + "@value": "कंप्यूटर माइक्रोफोन", + "@language": "hi" + }, + { + "@value": "计算机微软", + "@language": "zh" + }, + { + "@value": "Computer Microphone", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#computing", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#computer-keyboard", + "rdfs:label": [ + { + "@value": "Clavier d'ordinateur", + "@language": "fr" + }, + { + "@value": "Teclado", + "@language": "es" + }, + { + "@value": "Computer Tastatur", + "@language": "de" + }, + { + "@value": "Computer Keyboard", + "@language": "en" + }, + { + "@value": "لوحة مفاتيح الحاسوب", + "@language": "ar" + }, + { + "@value": "Computer Keyboard", + "@language": "ku" + }, + { + "@value": "Tastiera per computer", + "@language": "it" + }, + { + "@value": "Computer Keyboard", + "@language": "sw" + }, + { + "@value": "Teclado do computador", + "@language": "pt" + }, + { + "@value": "Computer Keyboard", + "@language": "oc" + }, + { + "@value": "Компьютерная клавиатура", + "@language": "ru" + }, + { + "@value": "Computer Keyboard", + "@language": "cy" + }, + { + "@value": "コンピュータ キーボード", + "@language": "ja" + }, + { + "@value": "Clár na dToghthóirí", + "@language": "ga" + }, + { + "@value": "कंप्यूटर कीबोर्ड", + "@language": "hi" + }, + { + "@value": "计算机关键板", + "@language": "zh" + }, + { + "@value": "Computer Keyboard", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#computing", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#mobile-phone", + "rdfs:label": [ + { + "@value": "Téléphone mobile", + "@language": "fr" + }, + { + "@value": "Teléfono móvil", + "@language": "es" + }, + { + "@value": "Mobiltelefon", + "@language": "de" + }, + { + "@value": "Mobile Phone", + "@language": "en" + }, + { + "@value": "الهاتف المحمول", + "@language": "ar" + }, + { + "@value": "Mobile Phone", + "@language": "ku" + }, + { + "@value": "Telefono cellulare", + "@language": "it" + }, + { + "@value": "Mobile Phone", + "@language": "sw" + }, + { + "@value": "Telefone móvel", + "@language": "pt" + }, + { + "@value": "Mobile Phone", + "@language": "oc" + }, + { + "@value": "Мобильный телефон", + "@language": "ru" + }, + { + "@value": "Mobile Phone", + "@language": "cy" + }, + { + "@value": "携帯電話", + "@language": "ja" + }, + { + "@value": "irl - Library Service", + "@language": "ga" + }, + { + "@value": "मोबाइल फ़ोन", + "@language": "hi" + }, + { + "@value": "流动博士", + "@language": "zh" + }, + { + "@value": "Mobile Phone", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#telecoms", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#dect-phone", + "rdfs:label": [ + { + "@value": "Téléphone de Dec", + "@language": "fr" + }, + { + "@value": "Teléfono DECT", + "@language": "es" + }, + { + "@value": "DECT-Telefon", + "@language": "de" + }, + { + "@value": "DECT Phone", + "@language": "en" + }, + { + "@value": "DECT Phone", + "@language": "ar" + }, + { + "@value": "DECT Phone", + "@language": "ku" + }, + { + "@value": "DEFINIZIONE Telefono", + "@language": "it" + }, + { + "@value": "DECT Phone", + "@language": "sw" + }, + { + "@value": "DECISÃO Telefone", + "@language": "pt" + }, + { + "@value": "DECT Phone", + "@language": "oc" + }, + { + "@value": "СДЕЛАТЬ Телефон", + "@language": "ru" + }, + { + "@value": "DECT Phone", + "@language": "cy" + }, + { + "@value": "インフォメーション 電話番号", + "@language": "ja" + }, + { + "@value": "CCTV irl - Library Service", + "@language": "ga" + }, + { + "@value": "डीईसीटी फ़ोन", + "@language": "hi" + }, + { + "@value": "导 言 博士", + "@language": "zh" + }, + { + "@value": "DECT Phone", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#telecoms", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#dect-base-station", + "rdfs:label": [ + { + "@value": "Station de base de Dec", + "@language": "fr" + }, + { + "@value": "Estación base DECT", + "@language": "es" + }, + { + "@value": "DECT-Basisstation", + "@language": "de" + }, + { + "@value": "DECT Base Station", + "@language": "en" + }, + { + "@value": "DECT محطة قاعدية", + "@language": "ar" + }, + { + "@value": "DECT Base Station", + "@language": "ku" + }, + { + "@value": "DEFINIZIONE Stazione di base", + "@language": "it" + }, + { + "@value": "DECT Base Station", + "@language": "sw" + }, + { + "@value": "DECISÃO Estação base", + "@language": "pt" + }, + { + "@value": "DECT Base Station", + "@language": "oc" + }, + { + "@value": "СДЕЛАТЬ Базовая станция", + "@language": "ru" + }, + { + "@value": "DECT Base Station", + "@language": "cy" + }, + { + "@value": "インフォメーション 基地局", + "@language": "ja" + }, + { + "@value": "CCTV Stáisiún Bunáite", + "@language": "ga" + }, + { + "@value": "डीईसीटी बेस स्टेशन", + "@language": "hi" + }, + { + "@value": "导 言 基地站", + "@language": "zh" + }, + { + "@value": "DECT Base Station", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#telecoms", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#network-router", + "rdfs:label": [ + { + "@value": "Routeur de réseau", + "@language": "fr" + }, + { + "@value": "Enrutador de red", + "@language": "es" + }, + { + "@value": "Netzwerkrouter", + "@language": "de" + }, + { + "@value": "Network Router", + "@language": "en" + }, + { + "@value": "الشبكة", + "@language": "ar" + }, + { + "@value": "Network Router", + "@language": "ku" + }, + { + "@value": "Router di rete", + "@language": "it" + }, + { + "@value": "Network Router", + "@language": "sw" + }, + { + "@value": "Roteador de rede", + "@language": "pt" + }, + { + "@value": "Network Router", + "@language": "oc" + }, + { + "@value": "Сеть Router", + "@language": "ru" + }, + { + "@value": "Network Router", + "@language": "cy" + }, + { + "@value": "ネットワーク ルーター", + "@language": "ja" + }, + { + "@value": "Líonra Ródaire", + "@language": "ga" + }, + { + "@value": "नेटवर्क रूटर", + "@language": "hi" + }, + { + "@value": "网络", + "@language": "zh" + }, + { + "@value": "Network Router", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#networking", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#wifi-router", + "rdfs:label": [ + { + "@value": "Routeur Wi-Fi", + "@language": "fr" + }, + { + "@value": "Router de wifi", + "@language": "es" + }, + { + "@value": "Wlan Router", + "@language": "de" + }, + { + "@value": "Wifi Router", + "@language": "en" + }, + { + "@value": "Wifi Router", + "@language": "ar" + }, + { + "@value": "Wifi Router", + "@language": "ku" + }, + { + "@value": "Router Wifi", + "@language": "it" + }, + { + "@value": "Wifi Router", + "@language": "sw" + }, + { + "@value": "Roteador de Wifi", + "@language": "pt" + }, + { + "@value": "Wifi Router", + "@language": "oc" + }, + { + "@value": "Wi-Fi роутер", + "@language": "ru" + }, + { + "@value": "Wifi Router", + "@language": "cy" + }, + { + "@value": "Wifiのルーター", + "@language": "ja" + }, + { + "@value": "Seirbhís do Chustaiméirí", + "@language": "ga" + }, + { + "@value": "वाईफ़ाई राउटर", + "@language": "hi" + }, + { + "@value": "Wifi Router", + "@language": "zh" + }, + { + "@value": "Wifi Router", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#networking", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#outdoor-router", + "rdfs:label": [ + { + "@value": "Outdoor Router", + "@language": "en" + }, + { + "@value": "في الهواء الطلق", + "@language": "ar" + }, + { + "@value": "Outdoor Router", + "@language": "ku" + }, + { + "@value": "Router exterior", + "@language": "es" + }, + { + "@value": "Router all'aperto", + "@language": "it" + }, + { + "@value": "Router im Freien", + "@language": "de" + }, + { + "@value": "Outdoor Router", + "@language": "sw" + }, + { + "@value": "Roteador ao ar livre", + "@language": "pt" + }, + { + "@value": "Outdoor Router", + "@language": "oc" + }, + { + "@value": "Открытый маршрутизатор", + "@language": "ru" + }, + { + "@value": "Outdoor Router", + "@language": "cy" + }, + { + "@value": "屋外のルーター", + "@language": "ja" + }, + { + "@value": "Seirbhís do Chustaiméirí", + "@language": "ga" + }, + { + "@value": "आउटडोर रूटर", + "@language": "hi" + }, + { + "@value": "户外区", + "@language": "zh" + }, + { + "@value": "Routeur extérieur", + "@language": "fr" + }, + { + "@value": "Outdoor Router", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#networking", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#poe-adapter", + "rdfs:label": [ + { + "@value": "Power Over Ethernet Adapter", + "@language": "en" + }, + { + "@value": "Power Over Ethernet Adapter", + "@language": "ar" + }, + { + "@value": "Power Over Ethernet Adapter", + "@language": "ku" + }, + { + "@value": "Adaptador Power Over Ethernet", + "@language": "es" + }, + { + "@value": "Adattatore di potenza su Ethernet", + "@language": "it" + }, + { + "@value": "Power Over Ethernet Adapter", + "@language": "de" + }, + { + "@value": "Power Over Ethernet Adapter", + "@language": "sw" + }, + { + "@value": "Alimentação sobre adaptador Ethernet", + "@language": "pt" + }, + { + "@value": "Power Over Ethernet Adapter", + "@language": "oc" + }, + { + "@value": "Мощность над адаптером Ethernet", + "@language": "ru" + }, + { + "@value": "Power Over Ethernet Adapter", + "@language": "cy" + }, + { + "@value": "イーサネット アダプター上の力", + "@language": "ja" + }, + { + "@value": "Cumhacht Thar Ethernet Adapter", + "@language": "ga" + }, + { + "@value": "ईथरनेट एडाप्टर पर पावर", + "@language": "hi" + }, + { + "@value": "权力", + "@language": "zh" + }, + { + "@value": "Adaptateur Ethernet Power Over", + "@language": "fr" + }, + { + "@value": "Power Over Ethernet Adapter", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#network-adapter", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#network-adapter", + "rdfs:label": [ + { + "@value": "Network Adapter", + "@language": "en" + }, + { + "@value": "الشبكة", + "@language": "ar" + }, + { + "@value": "Network Adapter", + "@language": "ku" + }, + { + "@value": "Adaptador de redes", + "@language": "es" + }, + { + "@value": "Adattatore di rete", + "@language": "it" + }, + { + "@value": "Netzwerkadapter", + "@language": "de" + }, + { + "@value": "Network Adapter", + "@language": "sw" + }, + { + "@value": "Adaptador de rede", + "@language": "pt" + }, + { + "@value": "Network Adapter", + "@language": "oc" + }, + { + "@value": "Сетевой адаптер", + "@language": "ru" + }, + { + "@value": "Network Adapter", + "@language": "cy" + }, + { + "@value": "ネットワークアダプタ", + "@language": "ja" + }, + { + "@value": "Líonra Adapter", + "@language": "ga" + }, + { + "@value": "नेटवर्क एडाप्टर", + "@language": "hi" + }, + { + "@value": "网络", + "@language": "zh" + }, + { + "@value": "Adaptateur réseau", + "@language": "fr" + }, + { + "@value": "Network Adapter", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#networking", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#cable-crimper", + "rdfs:label": [ + { + "@value": "Cable Crimper", + "@language": "en" + }, + { + "@value": "Cable Crimper", + "@language": "ar" + }, + { + "@value": "Cable Crimper", + "@language": "ku" + }, + { + "@value": "Cable Crimper", + "@language": "es" + }, + { + "@value": "Cavo di bloccaggio", + "@language": "it" + }, + { + "@value": "Kabel Crimper", + "@language": "de" + }, + { + "@value": "Cable Crimper", + "@language": "sw" + }, + { + "@value": "Máquina de montagem automática", + "@language": "pt" + }, + { + "@value": "Cable Crimper", + "@language": "oc" + }, + { + "@value": "Кабель Crimper", + "@language": "ru" + }, + { + "@value": "Cable Crimper", + "@language": "cy" + }, + { + "@value": "ケーブルクリンパー", + "@language": "ja" + }, + { + "@value": "Cábla Crimper", + "@language": "ga" + }, + { + "@value": "केबल Crimper", + "@language": "hi" + }, + { + "@value": "可起诉的犯罪", + "@language": "zh" + }, + { + "@value": "Crimper câble", + "@language": "fr" + }, + { + "@value": "Cable Crimper", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#networking", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#cable-stripper", + "rdfs:label": [ + { + "@value": "Cable Stripper", + "@language": "en" + }, + { + "@value": "جهاز لاصق", + "@language": "ar" + }, + { + "@value": "Cable Stripper", + "@language": "ku" + }, + { + "@value": "Cable Stripper", + "@language": "es" + }, + { + "@value": "Stripper cavi", + "@language": "it" + }, + { + "@value": "Kabelabstreifer", + "@language": "de" + }, + { + "@value": "Cable Stripper", + "@language": "sw" + }, + { + "@value": "Stripper de cabo", + "@language": "pt" + }, + { + "@value": "Cable Stripper", + "@language": "oc" + }, + { + "@value": "Кабель Stripper", + "@language": "ru" + }, + { + "@value": "Cable Stripper", + "@language": "cy" + }, + { + "@value": "ケーブルストリッパー", + "@language": "ja" + }, + { + "@value": "Cábla Stripper", + "@language": "ga" + }, + { + "@value": "केबल स्ट्रिपर", + "@language": "hi" + }, + { + "@value": "加沙地带", + "@language": "zh" + }, + { + "@value": "Striptease de câble", + "@language": "fr" + }, + { + "@value": "Cable Stripper", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#networking", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#cable-tester", + "rdfs:label": [ + { + "@value": "Cable Tester", + "@language": "en" + }, + { + "@value": "اختبارات الكبل", + "@language": "ar" + }, + { + "@value": "Cable Tester", + "@language": "ku" + }, + { + "@value": "Tester de cable", + "@language": "es" + }, + { + "@value": "Tester del cavo", + "@language": "it" + }, + { + "@value": "Kabelprüfgerät", + "@language": "de" + }, + { + "@value": "Cable Tester", + "@language": "sw" + }, + { + "@value": "Teste de cabo", + "@language": "pt" + }, + { + "@value": "Cable Tester", + "@language": "oc" + }, + { + "@value": "Кабельный тестер", + "@language": "ru" + }, + { + "@value": "Cable Tester", + "@language": "cy" + }, + { + "@value": "ケーブルテスター", + "@language": "ja" + }, + { + "@value": "An bhfuil a fhios agat na buntáistí a bhaineann...", + "@language": "ga" + }, + { + "@value": "केबल परीक्षक", + "@language": "hi" + }, + { + "@value": "可预测的年份", + "@language": "zh" + }, + { + "@value": "Cable Tester", + "@language": "fr" + }, + { + "@value": "Cable Tester", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#networking", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#scissors", + "rdfs:label": [ + { + "@value": "Scissors", + "@language": "en" + }, + { + "@value": "المقص", + "@language": "ar" + }, + { + "@value": "Scissors", + "@language": "ku" + }, + { + "@value": "Tijeras", + "@language": "es" + }, + { + "@value": "Forbici", + "@language": "it" + }, + { + "@value": "Scheren", + "@language": "de" + }, + { + "@value": "Scissors", + "@language": "sw" + }, + { + "@value": "Tesoura", + "@language": "pt" + }, + { + "@value": "Scissors", + "@language": "oc" + }, + { + "@value": "Сцензии", + "@language": "ru" + }, + { + "@value": "Scissors", + "@language": "cy" + }, + { + "@value": "シザーズ", + "@language": "ja" + }, + { + "@value": "Seirbhís do Chustaiméirí", + "@language": "ga" + }, + { + "@value": "कैंची", + "@language": "hi" + }, + { + "@value": "提名", + "@language": "zh" + }, + { + "@value": "Ciseaux", + "@language": "fr" + }, + { + "@value": "Scissors", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#cutting-tool", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#tweesers", + "rdfs:label": [ + { + "@value": "Tweesers", + "@language": "en" + }, + { + "@value": "Tweesers", + "@language": "ar" + }, + { + "@value": "Tweesers", + "@language": "ku" + }, + { + "@value": "Tweesers", + "@language": "es" + }, + { + "@value": "Tweesers", + "@language": "it" + }, + { + "@value": "Tweeser", + "@language": "de" + }, + { + "@value": "Tweesers", + "@language": "sw" + }, + { + "@value": "Tweesers", + "@language": "pt" + }, + { + "@value": "Tweesers", + "@language": "oc" + }, + { + "@value": "Tweesers", + "@language": "ru" + }, + { + "@value": "Tweesers", + "@language": "cy" + }, + { + "@value": "ツイート", + "@language": "ja" + }, + { + "@value": "Seirbhís do Chustaiméirí", + "@language": "ga" + }, + { + "@value": "ट्वीटर", + "@language": "hi" + }, + { + "@value": "泰韦尔", + "@language": "zh" + }, + { + "@value": "Tweesers", + "@language": "fr" + }, + { + "@value": "Tweesers", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#tweesers", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#electrical-tape", + "rdfs:label": [ + { + "@value": "Electrical Tape", + "@language": "en" + }, + { + "@value": "تاب كهربائي", + "@language": "ar" + }, + { + "@value": "Electrical Tape", + "@language": "ku" + }, + { + "@value": "Tapa eléctrica", + "@language": "es" + }, + { + "@value": "Nastro elettrico", + "@language": "it" + }, + { + "@value": "Elektrisches Band", + "@language": "de" + }, + { + "@value": "Electrical Tape", + "@language": "sw" + }, + { + "@value": "Fita elétrica", + "@language": "pt" + }, + { + "@value": "Electrical Tape", + "@language": "oc" + }, + { + "@value": "Электрическая лента", + "@language": "ru" + }, + { + "@value": "Electrical Tape", + "@language": "cy" + }, + { + "@value": "電気テープ", + "@language": "ja" + }, + { + "@value": "Téip Leictreach", + "@language": "ga" + }, + { + "@value": "विद्युत टेप", + "@language": "hi" + }, + { + "@value": "电力塔普", + "@language": "zh" + }, + { + "@value": "Tape électrique", + "@language": "fr" + }, + { + "@value": "Electrical Tape", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#tape", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#masking-tape", + "rdfs:label": [ + { + "@value": "Masking Tape", + "@language": "en" + }, + { + "@value": "تايب", + "@language": "ar" + }, + { + "@value": "Masking Tape", + "@language": "ku" + }, + { + "@value": "Masking Tape", + "@language": "es" + }, + { + "@value": "Nastro di mascheramento", + "@language": "it" + }, + { + "@value": "Maskenband", + "@language": "de" + }, + { + "@value": "Masking Tape", + "@language": "sw" + }, + { + "@value": "Fita de mascaramento", + "@language": "pt" + }, + { + "@value": "Masking Tape", + "@language": "oc" + }, + { + "@value": "Маскивая лента", + "@language": "ru" + }, + { + "@value": "Masking Tape", + "@language": "cy" + }, + { + "@value": "マスキングテープ", + "@language": "ja" + }, + { + "@value": "Téip Masking", + "@language": "ga" + }, + { + "@value": "मास्किंग टेप", + "@language": "hi" + }, + { + "@value": "马塞·塔普", + "@language": "zh" + }, + { + "@value": "Masking Tape", + "@language": "fr" + }, + { + "@value": "Masking Tape", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#tape", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#plumbers-tape", + "rdfs:label": [ + { + "@value": "Plumbers Tape", + "@language": "en" + }, + { + "@value": "سائل", + "@language": "ar" + }, + { + "@value": "Plumbers Tape", + "@language": "ku" + }, + { + "@value": "Plumbers Tape", + "@language": "es" + }, + { + "@value": "Nastro di legno", + "@language": "it" + }, + { + "@value": "Klempnerband", + "@language": "de" + }, + { + "@value": "Plumbers Tape", + "@language": "sw" + }, + { + "@value": "Fita de Encanadores", + "@language": "pt" + }, + { + "@value": "Plumbers Tape", + "@language": "oc" + }, + { + "@value": "Пломберы лента", + "@language": "ru" + }, + { + "@value": "Plumbers Tape", + "@language": "cy" + }, + { + "@value": "配管テープ", + "@language": "ja" + }, + { + "@value": "taiseachas aeir: fliuch", + "@language": "ga" + }, + { + "@value": "प्लंबर टेप", + "@language": "hi" + }, + { + "@value": "Plumbers Tape", + "@language": "zh" + }, + { + "@value": "Plumbers Tape", + "@language": "fr" + }, + { + "@value": "Plumbers Tape", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#tape", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#duct-tape", + "rdfs:label": [ + { + "@value": "Duct Tape", + "@language": "en" + }, + { + "@value": "Duct Tape", + "@language": "ar" + }, + { + "@value": "Duct Tape", + "@language": "ku" + }, + { + "@value": "Duct Tape", + "@language": "es" + }, + { + "@value": "Nastro del condotto", + "@language": "it" + }, + { + "@value": "Duct Tape", + "@language": "de" + }, + { + "@value": "Duct Tape", + "@language": "sw" + }, + { + "@value": "Fita adesiva", + "@language": "pt" + }, + { + "@value": "Duct Tape", + "@language": "oc" + }, + { + "@value": "Duct лента", + "@language": "ru" + }, + { + "@value": "Duct Tape", + "@language": "cy" + }, + { + "@value": "ダクトテープ", + "@language": "ja" + }, + { + "@value": "taiseachas aeir: fliuch", + "@language": "ga" + }, + { + "@value": "डक्ट टेप", + "@language": "hi" + }, + { + "@value": "Duct Tape", + "@language": "zh" + }, + { + "@value": "Robinet de canal", + "@language": "fr" + }, + { + "@value": "Duct Tape", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#tape", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#chordless-drill", + "rdfs:label": [ + { + "@value": "Chordless Drill", + "@language": "en" + }, + { + "@value": "Drill", + "@language": "ar" + }, + { + "@value": "Chordless Drill", + "@language": "ku" + }, + { + "@value": "Chordless Drill", + "@language": "es" + }, + { + "@value": "Trapano senza coro", + "@language": "it" + }, + { + "@value": "Chorfreier Bohrer", + "@language": "de" + }, + { + "@value": "Chordless Drill", + "@language": "sw" + }, + { + "@value": "Broca sem fio", + "@language": "pt" + }, + { + "@value": "Chordless Drill", + "@language": "oc" + }, + { + "@value": "Безсловная дрель", + "@language": "ru" + }, + { + "@value": "Chordless Drill", + "@language": "cy" + }, + { + "@value": "Chordlessのドリル", + "@language": "ja" + }, + { + "@value": "Druileáil Chordless", + "@language": "ga" + }, + { + "@value": "ताररहित ड्रिल", + "@language": "hi" + }, + { + "@value": "选择无缝的D钻井", + "@language": "zh" + }, + { + "@value": "Chordless Drill", + "@language": "fr" + }, + { + "@value": "Chordless Drill", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#drill", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#nails", + "rdfs:label": [ + { + "@value": "Nails", + "@language": "en" + }, + { + "@value": "النيل", + "@language": "ar" + }, + { + "@value": "Nails", + "@language": "ku" + }, + { + "@value": "Nails", + "@language": "es" + }, + { + "@value": "Unghie", + "@language": "it" + }, + { + "@value": "Nägel", + "@language": "de" + }, + { + "@value": "Nails", + "@language": "sw" + }, + { + "@value": "Unhas", + "@language": "pt" + }, + { + "@value": "Nails", + "@language": "oc" + }, + { + "@value": "Гвозди", + "@language": "ru" + }, + { + "@value": "Nails", + "@language": "cy" + }, + { + "@value": "ネイル", + "@language": "ja" + }, + { + "@value": "taiseachas aeir: fliuch", + "@language": "ga" + }, + { + "@value": "नाखून", + "@language": "hi" + }, + { + "@value": "Nails", + "@language": "zh" + }, + { + "@value": "Nails", + "@language": "fr" + }, + { + "@value": "Nails", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#fixing", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#screws", + "rdfs:label": [ + { + "@value": "Screws", + "@language": "en" + }, + { + "@value": "اللعنة", + "@language": "ar" + }, + { + "@value": "Screws", + "@language": "ku" + }, + { + "@value": "Mierda.", + "@language": "es" + }, + { + "@value": "Viti", + "@language": "it" + }, + { + "@value": "Schrauben", + "@language": "de" + }, + { + "@value": "Screws", + "@language": "sw" + }, + { + "@value": "Parafusos", + "@language": "pt" + }, + { + "@value": "Screws", + "@language": "oc" + }, + { + "@value": "Винты", + "@language": "ru" + }, + { + "@value": "Screws", + "@language": "cy" + }, + { + "@value": "ネジ", + "@language": "ja" + }, + { + "@value": "Seirbhís do Chustaiméirí", + "@language": "ga" + }, + { + "@value": "पेंच", + "@language": "hi" + }, + { + "@value": "Screws", + "@language": "zh" + }, + { + "@value": "Screws", + "@language": "fr" + }, + { + "@value": "Screws", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#fixing", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#bolts", + "rdfs:label": [ + { + "@value": "Bolts", + "@language": "en" + }, + { + "@value": "الفول", + "@language": "ar" + }, + { + "@value": "Bolts", + "@language": "ku" + }, + { + "@value": "Bolts", + "@language": "es" + }, + { + "@value": "Bulloni", + "@language": "it" + }, + { + "@value": "Bolzen", + "@language": "de" + }, + { + "@value": "Bolts", + "@language": "sw" + }, + { + "@value": "Parafusos", + "@language": "pt" + }, + { + "@value": "Bolts", + "@language": "oc" + }, + { + "@value": "Болты", + "@language": "ru" + }, + { + "@value": "Bolts", + "@language": "cy" + }, + { + "@value": "ボルト", + "@language": "ja" + }, + { + "@value": "Seirbhís do Chustaiméirí", + "@language": "ga" + }, + { + "@value": "बोल्ट", + "@language": "hi" + }, + { + "@value": "Bolts", + "@language": "zh" + }, + { + "@value": "Bolts", + "@language": "fr" + }, + { + "@value": "Bolts", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#fixing", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#battery", + "rdfs:label": [ + { + "@value": "Batterie", + "@language": "fr" + }, + { + "@value": "Batería", + "@language": "es" + }, + { + "@value": "Batterie", + "@language": "de" + }, + { + "@value": "Battery", + "@language": "en" + }, + { + "@value": "البطارية", + "@language": "ar" + }, + { + "@value": "Battery", + "@language": "ku" + }, + { + "@value": "Batteria", + "@language": "it" + }, + { + "@value": "Battery", + "@language": "sw" + }, + { + "@value": "Bateria", + "@language": "pt" + }, + { + "@value": "Battery", + "@language": "oc" + }, + { + "@value": "Батарея", + "@language": "ru" + }, + { + "@value": "Battery", + "@language": "cy" + }, + { + "@value": "バッテリー", + "@language": "ja" + }, + { + "@value": "Cén fáth ar chóir dom...", + "@language": "ga" + }, + { + "@value": "बैटरी", + "@language": "hi" + }, + { + "@value": "包 耳", + "@language": "zh" + }, + { + "@value": "Battery", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#battery", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#cable", + "rdfs:label": [ + { + "@value": "Câble", + "@language": "fr" + }, + { + "@value": "Cable", + "@language": "es" + }, + { + "@value": "Kabel", + "@language": "de" + }, + { + "@value": "Cable", + "@language": "en" + }, + { + "@value": "Cable", + "@language": "ar" + }, + { + "@value": "Cable", + "@language": "ku" + }, + { + "@value": "Cavo", + "@language": "it" + }, + { + "@value": "Cable", + "@language": "sw" + }, + { + "@value": "Cabo de cabo", + "@language": "pt" + }, + { + "@value": "Cable", + "@language": "oc" + }, + { + "@value": "Кабель", + "@language": "ru" + }, + { + "@value": "Cable", + "@language": "cy" + }, + { + "@value": "ケーブル", + "@language": "ja" + }, + { + "@value": "Cábla", + "@language": "ga" + }, + { + "@value": "केबल", + "@language": "hi" + }, + { + "@value": "可核查", + "@language": "zh" + }, + { + "@value": "Cable", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#cable", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#microcontroller", + "rdfs:label": [ + { + "@value": "Microcontrôleur", + "@language": "fr" + }, + { + "@value": "Microcontroladora", + "@language": "es" + }, + { + "@value": "Mikrocontroller", + "@language": "de" + }, + { + "@value": "Microcontroller", + "@language": "en" + }, + { + "@value": "Microcontroller", + "@language": "ar" + }, + { + "@value": "Microcontroller", + "@language": "ku" + }, + { + "@value": "Microcontrollore", + "@language": "it" + }, + { + "@value": "Microcontroller", + "@language": "sw" + }, + { + "@value": "Microcontrolador", + "@language": "pt" + }, + { + "@value": "Microcontroller", + "@language": "oc" + }, + { + "@value": "Микроконтроллер", + "@language": "ru" + }, + { + "@value": "Microcontroller", + "@language": "cy" + }, + { + "@value": "マイクロコントローラ", + "@language": "ja" + }, + { + "@value": "Micrimhilseogra", + "@language": "ga" + }, + { + "@value": "माइक्रोकंट्रोलर", + "@language": "hi" + }, + { + "@value": "微额控制区", + "@language": "zh" + }, + { + "@value": "Microcontroller", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#computing", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#single-board-computer", + "rdfs:label": [ + { + "@value": "Ordinateur unique", + "@language": "fr" + }, + { + "@value": "Computadora de una sola tabla", + "@language": "es" + }, + { + "@value": "Single-Board-Computer", + "@language": "de" + }, + { + "@value": "Single Board Computer", + "@language": "en" + }, + { + "@value": "حاسوب المجلس الوحيد", + "@language": "ar" + }, + { + "@value": "Single Board Computer", + "@language": "ku" + }, + { + "@value": "Computer di bordo singolo", + "@language": "it" + }, + { + "@value": "Single Board Computer", + "@language": "sw" + }, + { + "@value": "Computador de placa única", + "@language": "pt" + }, + { + "@value": "Single Board Computer", + "@language": "oc" + }, + { + "@value": "Single Board Компьютер", + "@language": "ru" + }, + { + "@value": "Single Board Computer", + "@language": "cy" + }, + { + "@value": "シングルボードコンピュータ", + "@language": "ja" + }, + { + "@value": "Bord Aonair Ríomhaire", + "@language": "ga" + }, + { + "@value": "एकल बोर्ड कंप्यूटर", + "@language": "hi" + }, + { + "@value": "单一委员会", + "@language": "zh" + }, + { + "@value": "Single Board Computer", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#computing", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#screwdriver", + "rdfs:label": [ + { + "@value": "Tournevis", + "@language": "fr" + }, + { + "@value": "Destornillador", + "@language": "es" + }, + { + "@value": "Schraubendreher", + "@language": "de" + }, + { + "@value": "Screwdriver", + "@language": "en" + }, + { + "@value": "Screwdriver", + "@language": "ar" + }, + { + "@value": "Screwdriver", + "@language": "ku" + }, + { + "@value": "Cacciavite", + "@language": "it" + }, + { + "@value": "Screwdriver", + "@language": "sw" + }, + { + "@value": "Chave de fenda", + "@language": "pt" + }, + { + "@value": "Screwdriver", + "@language": "oc" + }, + { + "@value": "Отвертка", + "@language": "ru" + }, + { + "@value": "Screwdriver", + "@language": "cy" + }, + { + "@value": "スクリュードライバー", + "@language": "ja" + }, + { + "@value": "Scriú-scriú", + "@language": "ga" + }, + { + "@value": "पेचकश", + "@language": "hi" + }, + { + "@value": "Screwdriver", + "@language": "zh" + }, + { + "@value": "Screwdriver", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#screwdriver", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#spanner", + "rdfs:label": [ + { + "@value": "Clé", + "@language": "fr" + }, + { + "@value": "Llave", + "@language": "es" + }, + { + "@value": "Schlüssel", + "@language": "de" + }, + { + "@value": "Spanner", + "@language": "en" + }, + { + "@value": "Spanner", + "@language": "ar" + }, + { + "@value": "Spanner", + "@language": "ku" + }, + { + "@value": "Spanner", + "@language": "it" + }, + { + "@value": "Spanner", + "@language": "sw" + }, + { + "@value": "Espanhóis", + "@language": "pt" + }, + { + "@value": "Spanner", + "@language": "oc" + }, + { + "@value": "Спанер", + "@language": "ru" + }, + { + "@value": "Spanner", + "@language": "cy" + }, + { + "@value": "スパナー", + "@language": "ja" + }, + { + "@value": "Spanner", + "@language": "ga" + }, + { + "@value": "स्पैनर", + "@language": "hi" + }, + { + "@value": "扩大伙伴", + "@language": "zh" + }, + { + "@value": "Spanner", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#spanner", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#vise-grips", + "rdfs:label": [ + { + "@value": "Vise Grips", + "@language": "en" + }, + { + "@value": "Vise Grips", + "@language": "ar" + }, + { + "@value": "Vise Grips", + "@language": "ku" + }, + { + "@value": "Vise Grips", + "@language": "es" + }, + { + "@value": "Griglia di vise", + "@language": "it" + }, + { + "@value": "Vise Grips", + "@language": "de" + }, + { + "@value": "Vise Grips", + "@language": "sw" + }, + { + "@value": "Apertos de Vise", + "@language": "pt" + }, + { + "@value": "Vise Grips", + "@language": "oc" + }, + { + "@value": "Вис Грипс", + "@language": "ru" + }, + { + "@value": "Vise Grips", + "@language": "cy" + }, + { + "@value": "ヴァイズグリップ", + "@language": "ja" + }, + { + "@value": "Seirbhís do Chustaiméirí", + "@language": "ga" + }, + { + "@value": "Vise Grips", + "@language": "hi" + }, + { + "@value": "B. 签证", + "@language": "zh" + }, + { + "@value": "Vise Grips", + "@language": "fr" + }, + { + "@value": "Vise Grips", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#pliers", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#adjustable-spanner", + "rdfs:label": [ + { + "@value": "Adjustable Spanner", + "@language": "en" + }, + { + "@value": "Spanner", + "@language": "ar" + }, + { + "@value": "Adjustable Spanner", + "@language": "ku" + }, + { + "@value": "Spanner ajustable", + "@language": "es" + }, + { + "@value": "Spanner regolabile", + "@language": "it" + }, + { + "@value": "Einstellbare Spannvorrichtung", + "@language": "de" + }, + { + "@value": "Adjustable Spanner", + "@language": "sw" + }, + { + "@value": "Spanner ajustável", + "@language": "pt" + }, + { + "@value": "Adjustable Spanner", + "@language": "oc" + }, + { + "@value": "Регулируемый Spanner", + "@language": "ru" + }, + { + "@value": "Adjustable Spanner", + "@language": "cy" + }, + { + "@value": "調節可能なスパナー", + "@language": "ja" + }, + { + "@value": "Spanner inchoigeartaithe", + "@language": "ga" + }, + { + "@value": "समायोज्य स्पैनर", + "@language": "hi" + }, + { + "@value": "A. 合理的扩大能力", + "@language": "zh" + }, + { + "@value": "Spanner réglable", + "@language": "fr" + }, + { + "@value": "Adjustable Spanner", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#spanner", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#hose-clamp", + "rdfs:label": [ + { + "@value": "Hose Clamp", + "@language": "en" + }, + { + "@value": "مشبك هوس", + "@language": "ar" + }, + { + "@value": "Hose Clamp", + "@language": "ku" + }, + { + "@value": "Abrazadera de manguera", + "@language": "es" + }, + { + "@value": "Morsetto per tubi", + "@language": "it" + }, + { + "@value": "Schlauchklemme", + "@language": "de" + }, + { + "@value": "Hose Clamp", + "@language": "sw" + }, + { + "@value": "Braçadeira de mangueira", + "@language": "pt" + }, + { + "@value": "Hose Clamp", + "@language": "oc" + }, + { + "@value": "Шланг Зажим", + "@language": "ru" + }, + { + "@value": "Hose Clamp", + "@language": "cy" + }, + { + "@value": "ホースクランプ", + "@language": "ja" + }, + { + "@value": "riachtanais uisce: measartha", + "@language": "ga" + }, + { + "@value": "नली क्लैंप", + "@language": "hi" + }, + { + "@value": "Hose Clamp", + "@language": "zh" + }, + { + "@value": "Hose Clamp", + "@language": "fr" + }, + { + "@value": "Hose Clamp", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#fixing", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#antenna-mast", + "rdfs:label": [ + { + "@value": "Antenna Mast", + "@language": "en" + }, + { + "@value": "Antenna Mast", + "@language": "ar" + }, + { + "@value": "Antenna Mast", + "@language": "ku" + }, + { + "@value": "Antenna Mast", + "@language": "es" + }, + { + "@value": "Antenna Mast", + "@language": "it" + }, + { + "@value": "Antenne Mast", + "@language": "de" + }, + { + "@value": "Antenna Mast", + "@language": "sw" + }, + { + "@value": "Mastro de antena", + "@language": "pt" + }, + { + "@value": "Antenna Mast", + "@language": "oc" + }, + { + "@value": "Антенна Маст", + "@language": "ru" + }, + { + "@value": "Antenna Mast", + "@language": "cy" + }, + { + "@value": "アンテナ マスト", + "@language": "ja" + }, + { + "@value": "Seirbhís do Chustaiméirí", + "@language": "ga" + }, + { + "@value": "एंटीना मस्त", + "@language": "hi" + }, + { + "@value": "Antenna Mast", + "@language": "zh" + }, + { + "@value": "Antenna Mast", + "@language": "fr" + }, + { + "@value": "Antenna Mast", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#networking", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#washers", + "rdfs:label": [ + { + "@value": "Washers", + "@language": "en" + }, + { + "@value": "الغسالات", + "@language": "ar" + }, + { + "@value": "Washers", + "@language": "ku" + }, + { + "@value": "Lavadoras", + "@language": "es" + }, + { + "@value": "Rondelle", + "@language": "it" + }, + { + "@value": "Unterlegscheiben", + "@language": "de" + }, + { + "@value": "Washers", + "@language": "sw" + }, + { + "@value": "Arruelas", + "@language": "pt" + }, + { + "@value": "Washers", + "@language": "oc" + }, + { + "@value": "Стиральная", + "@language": "ru" + }, + { + "@value": "Washers", + "@language": "cy" + }, + { + "@value": "ウォッシャー", + "@language": "ja" + }, + { + "@value": "Seirbhís do Chustaiméirí", + "@language": "ga" + }, + { + "@value": "वाशर", + "@language": "hi" + }, + { + "@value": "什图尔", + "@language": "zh" + }, + { + "@value": "Lave-linge", + "@language": "fr" + }, + { + "@value": "Washers", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#fixing", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#zip-ties", + "rdfs:label": [ + { + "@value": "Zip Ties", + "@language": "en" + }, + { + "@value": "Zip Ties", + "@language": "ar" + }, + { + "@value": "Zip Ties", + "@language": "ku" + }, + { + "@value": "Tintes de cremallera", + "@language": "es" + }, + { + "@value": "Cerniera", + "@language": "it" + }, + { + "@value": "Zangen-Kissen", + "@language": "de" + }, + { + "@value": "Zip Ties", + "@language": "sw" + }, + { + "@value": "Ties de zíper", + "@language": "pt" + }, + { + "@value": "Zip Ties", + "@language": "oc" + }, + { + "@value": "Зип Тиси", + "@language": "ru" + }, + { + "@value": "Zip Ties", + "@language": "cy" + }, + { + "@value": "ジップタイズ", + "@language": "ja" + }, + { + "@value": "Ties le Zip", + "@language": "ga" + }, + { + "@value": "ज़िप टाई", + "@language": "hi" + }, + { + "@value": "Zip Ties", + "@language": "zh" + }, + { + "@value": "Zip Ties", + "@language": "fr" + }, + { + "@value": "Zip Ties", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#fixing", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#cable-staples", + "rdfs:label": [ + { + "@value": "Cable Staples", + "@language": "en" + }, + { + "@value": "Cable Staples", + "@language": "ar" + }, + { + "@value": "Cable Staples", + "@language": "ku" + }, + { + "@value": "Cable Staples", + "@language": "es" + }, + { + "@value": "Cavi di ricambio", + "@language": "it" + }, + { + "@value": "Kabeleinführungen", + "@language": "de" + }, + { + "@value": "Cable Staples", + "@language": "sw" + }, + { + "@value": "Conjuntos de cabos", + "@language": "pt" + }, + { + "@value": "Cable Staples", + "@language": "oc" + }, + { + "@value": "Кабельные скобы", + "@language": "ru" + }, + { + "@value": "Cable Staples", + "@language": "cy" + }, + { + "@value": "ケーブルのステープル", + "@language": "ja" + }, + { + "@value": "Uirlisí ilchuspóireacha", + "@language": "ga" + }, + { + "@value": "केबल स्टेपल", + "@language": "hi" + }, + { + "@value": "可核查的物质", + "@language": "zh" + }, + { + "@value": "Agrafes de câble", + "@language": "fr" + }, + { + "@value": "Cable Staples", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#fixing", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#cable-clip", + "rdfs:label": [ + { + "@value": "Cable Fastener Clip", + "@language": "en" + }, + { + "@value": "Cable Fastener Clip", + "@language": "ar" + }, + { + "@value": "Cable Fastener Clip", + "@language": "ku" + }, + { + "@value": "Clip de cable de separación", + "@language": "es" + }, + { + "@value": "Clip di fissaggio del cavo", + "@language": "it" + }, + { + "@value": "Kabelverbinder Clip", + "@language": "de" + }, + { + "@value": "Cable Fastener Clip", + "@language": "sw" + }, + { + "@value": "Clipe de fixação de cabo", + "@language": "pt" + }, + { + "@value": "Cable Fastener Clip", + "@language": "oc" + }, + { + "@value": "Кабель Fastener Clip", + "@language": "ru" + }, + { + "@value": "Cable Fastener Clip", + "@language": "cy" + }, + { + "@value": "ケーブルの締める物クリップ", + "@language": "ja" + }, + { + "@value": "Seirbhís do Chustaiméirí", + "@language": "ga" + }, + { + "@value": "केबल फास्टनर क्लिप", + "@language": "hi" + }, + { + "@value": "可兑换Fsten Clip", + "@language": "zh" + }, + { + "@value": "Câble de fixation Clip", + "@language": "fr" + }, + { + "@value": "Cable Fastener Clip", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#fixing", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#hand-truck", + "rdfs:label": [ + { + "@value": "Hand Truck", + "@language": "en" + }, + { + "@value": "شاحنة يدوية", + "@language": "ar" + }, + { + "@value": "Hand Truck", + "@language": "ku" + }, + { + "@value": "Camión de mano", + "@language": "es" + }, + { + "@value": "camion della mano", + "@language": "it" + }, + { + "@value": "Handwagen", + "@language": "de" + }, + { + "@value": "Hand Truck", + "@language": "sw" + }, + { + "@value": "Caminhão de mão", + "@language": "pt" + }, + { + "@value": "Hand Truck", + "@language": "oc" + }, + { + "@value": "Ручная тележка", + "@language": "ru" + }, + { + "@value": "Hand Truck", + "@language": "cy" + }, + { + "@value": "ハンドトラック", + "@language": "ja" + }, + { + "@value": "Truck Hand", + "@language": "ga" + }, + { + "@value": "हाथ ट्रक", + "@language": "hi" + }, + { + "@value": "Hand Truck", + "@language": "zh" + }, + { + "@value": "Hand Truck", + "@language": "fr" + }, + { + "@value": "Hand Truck", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#trolley", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#hammer", + "rdfs:label": [ + { + "@value": "Marteau", + "@language": "fr" + }, + { + "@value": "Martillo", + "@language": "es" + }, + { + "@value": "Hammer", + "@language": "de" + }, + { + "@value": "Hammer", + "@language": "en" + }, + { + "@value": "هامر", + "@language": "ar" + }, + { + "@value": "Hammer", + "@language": "ku" + }, + { + "@value": "Martello", + "@language": "it" + }, + { + "@value": "Hammer", + "@language": "sw" + }, + { + "@value": "Martelo de martelo", + "@language": "pt" + }, + { + "@value": "Hammer", + "@language": "oc" + }, + { + "@value": "молот", + "@language": "ru" + }, + { + "@value": "Hammer", + "@language": "cy" + }, + { + "@value": "ハンマー", + "@language": "ja" + }, + { + "@value": "tréimhse saoil: ilbhliantúil", + "@language": "ga" + }, + { + "@value": "हथौड़ा", + "@language": "hi" + }, + { + "@value": "Hammer", + "@language": "zh" + }, + { + "@value": "Hammer", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#hammer", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#drill-bit", + "rdfs:label": [ + { + "@value": "Foreuse", + "@language": "fr" + }, + { + "@value": "Broca", + "@language": "es" + }, + { + "@value": "Bohrer", + "@language": "de" + }, + { + "@value": "Drill bit", + "@language": "en" + }, + { + "@value": "دريل قليلا", + "@language": "ar" + }, + { + "@value": "Drill bit", + "@language": "ku" + }, + { + "@value": "Punteggio di perforazione", + "@language": "it" + }, + { + "@value": "Drill bit", + "@language": "sw" + }, + { + "@value": "bit de broca", + "@language": "pt" + }, + { + "@value": "Drill bit", + "@language": "oc" + }, + { + "@value": "Сверлить бит", + "@language": "ru" + }, + { + "@value": "Drill bit", + "@language": "cy" + }, + { + "@value": "ドリルビット", + "@language": "ja" + }, + { + "@value": "giotán druil", + "@language": "ga" + }, + { + "@value": "ड्रिल बिट", + "@language": "hi" + }, + { + "@value": "导 言", + "@language": "zh" + }, + { + "@value": "Drill bit", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#drill", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#socket-set", + "rdfs:label": [ + { + "@value": "Prise de courant", + "@language": "fr" + }, + { + "@value": "Conjunto de zócalos", + "@language": "es" + }, + { + "@value": "Socken-Set", + "@language": "de" + }, + { + "@value": "Socket Set", + "@language": "en" + }, + { + "@value": "مجموعة جوكيت", + "@language": "ar" + }, + { + "@value": "Socket Set", + "@language": "ku" + }, + { + "@value": "Set di presa", + "@language": "it" + }, + { + "@value": "Socket Set", + "@language": "sw" + }, + { + "@value": "Conjunto de soquete", + "@language": "pt" + }, + { + "@value": "Socket Set", + "@language": "oc" + }, + { + "@value": "Комплект розетки", + "@language": "ru" + }, + { + "@value": "Socket Set", + "@language": "cy" + }, + { + "@value": "ソケットセット", + "@language": "ja" + }, + { + "@value": "Socraigh Soicéad", + "@language": "ga" + }, + { + "@value": "सॉकेट सेट", + "@language": "hi" + }, + { + "@value": "导 言", + "@language": "zh" + }, + { + "@value": "Socket Set", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#socket-set", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#paintbrush", + "rdfs:label": [ + { + "@value": "Pinceau", + "@language": "fr" + }, + { + "@value": "Cepillo de pintura", + "@language": "es" + }, + { + "@value": "Pinsel", + "@language": "de" + }, + { + "@value": "Paintbrush", + "@language": "en" + }, + { + "@value": "الطلاء", + "@language": "ar" + }, + { + "@value": "Paintbrush", + "@language": "ku" + }, + { + "@value": "Spazzola di vernice", + "@language": "it" + }, + { + "@value": "Paintbrush", + "@language": "sw" + }, + { + "@value": "Escova de tinta", + "@language": "pt" + }, + { + "@value": "Paintbrush", + "@language": "oc" + }, + { + "@value": "Paintbrush", + "@language": "ru" + }, + { + "@value": "Paintbrush", + "@language": "cy" + }, + { + "@value": "ペイントブラシ", + "@language": "ja" + }, + { + "@value": "cineál gas: in airde", + "@language": "ga" + }, + { + "@value": "पेंटब्रश", + "@language": "hi" + }, + { + "@value": "Paintbrush", + "@language": "zh" + }, + { + "@value": "Paintbrush", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#painting", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#paint-roller", + "rdfs:label": [ + { + "@value": "Rouleau de peinture", + "@language": "fr" + }, + { + "@value": "Rodillo", + "@language": "es" + }, + { + "@value": "Farbrolle", + "@language": "de" + }, + { + "@value": "Paint Roller", + "@language": "en" + }, + { + "@value": "الطلاء", + "@language": "ar" + }, + { + "@value": "Paint Roller", + "@language": "ku" + }, + { + "@value": "Rullo di vernice", + "@language": "it" + }, + { + "@value": "Paint Roller", + "@language": "sw" + }, + { + "@value": "Rolo de tinta", + "@language": "pt" + }, + { + "@value": "Paint Roller", + "@language": "oc" + }, + { + "@value": "Paint ролик", + "@language": "ru" + }, + { + "@value": "Paint Roller", + "@language": "cy" + }, + { + "@value": "ペイントローラー", + "@language": "ja" + }, + { + "@value": "Paint Roller", + "@language": "ga" + }, + { + "@value": "पेंट रोलर", + "@language": "hi" + }, + { + "@value": "减 减 金", + "@language": "zh" + }, + { + "@value": "Paint Roller", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#painting", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#water-barrel", + "rdfs:label": [ + { + "@value": "Baril d'eau", + "@language": "fr" + }, + { + "@value": "Barril de agua", + "@language": "es" + }, + { + "@value": "Wasserfass", + "@language": "de" + }, + { + "@value": "Water Barrel", + "@language": "en" + }, + { + "@value": "المياه", + "@language": "ar" + }, + { + "@value": "Water Barrel", + "@language": "ku" + }, + { + "@value": "Barrello dell'acqua", + "@language": "it" + }, + { + "@value": "Water Barrel", + "@language": "sw" + }, + { + "@value": "Barril de água", + "@language": "pt" + }, + { + "@value": "Water Barrel", + "@language": "oc" + }, + { + "@value": "Баррель воды", + "@language": "ru" + }, + { + "@value": "Water Barrel", + "@language": "cy" + }, + { + "@value": "水バレル", + "@language": "ja" + }, + { + "@value": "Uisce agus Séarachas", + "@language": "ga" + }, + { + "@value": "जल बैरल", + "@language": "hi" + }, + { + "@value": "水巴利", + "@language": "zh" + }, + { + "@value": "Water Barrel", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#container", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#thermostat", + "rdfs:label": [ + { + "@value": "Thermostat", + "@language": "fr" + }, + { + "@value": "Termostato", + "@language": "es" + }, + { + "@value": "Thermostat", + "@language": "de" + }, + { + "@value": "Thermostat", + "@language": "en" + }, + { + "@value": "Thermostat", + "@language": "ar" + }, + { + "@value": "Thermostat", + "@language": "ku" + }, + { + "@value": "Termostato", + "@language": "it" + }, + { + "@value": "Thermostat", + "@language": "sw" + }, + { + "@value": "Termostato", + "@language": "pt" + }, + { + "@value": "Thermostat", + "@language": "oc" + }, + { + "@value": "Термостат", + "@language": "ru" + }, + { + "@value": "Thermostat", + "@language": "cy" + }, + { + "@value": "サーモスタット", + "@language": "ja" + }, + { + "@value": "Teirmeach", + "@language": "ga" + }, + { + "@value": "थर्मोस्टेट", + "@language": "hi" + }, + { + "@value": "动荡局势", + "@language": "zh" + }, + { + "@value": "Thermostat", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#sensor", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#thermometer", + "rdfs:label": [ + { + "@value": "Thermomètre", + "@language": "fr" + }, + { + "@value": "Termómetro", + "@language": "es" + }, + { + "@value": "Thermometer", + "@language": "de" + }, + { + "@value": "Thermometer", + "@language": "en" + }, + { + "@value": "مقياس الحرارة", + "@language": "ar" + }, + { + "@value": "Thermometer", + "@language": "ku" + }, + { + "@value": "Termometro", + "@language": "it" + }, + { + "@value": "Thermometer", + "@language": "sw" + }, + { + "@value": "Termómetro", + "@language": "pt" + }, + { + "@value": "Thermometer", + "@language": "oc" + }, + { + "@value": "Термометр", + "@language": "ru" + }, + { + "@value": "Thermometer", + "@language": "cy" + }, + { + "@value": "温度計", + "@language": "ja" + }, + { + "@value": "Teirmiméadar", + "@language": "ga" + }, + { + "@value": "थर्मामीटर", + "@language": "hi" + }, + { + "@value": "2. 动荡墓地", + "@language": "zh" + }, + { + "@value": "Thermometer", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#sensor", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#gas-sensor", + "rdfs:label": [ + { + "@value": "Capteur de gaz", + "@language": "fr" + }, + { + "@value": "Sensor de gas", + "@language": "es" + }, + { + "@value": "Gassensor", + "@language": "de" + }, + { + "@value": "Gas Sensor", + "@language": "en" + }, + { + "@value": "Gas Sensor", + "@language": "ar" + }, + { + "@value": "Gas Sensor", + "@language": "ku" + }, + { + "@value": "Sensore di gas", + "@language": "it" + }, + { + "@value": "Gas Sensor", + "@language": "sw" + }, + { + "@value": "Sensor de gás", + "@language": "pt" + }, + { + "@value": "Gas Sensor", + "@language": "oc" + }, + { + "@value": "Датчик газа", + "@language": "ru" + }, + { + "@value": "Gas Sensor", + "@language": "cy" + }, + { + "@value": "ガスセンサー", + "@language": "ja" + }, + { + "@value": "Braiteoir Gáis", + "@language": "ga" + }, + { + "@value": "गैस सेंसर", + "@language": "hi" + }, + { + "@value": "Gs ensor", + "@language": "zh" + }, + { + "@value": "Gas Sensor", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#sensor", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#fire-alarm", + "rdfs:label": [ + { + "@value": "Alarme incendie", + "@language": "fr" + }, + { + "@value": "Alarma de incendios", + "@language": "es" + }, + { + "@value": "Feueralarm", + "@language": "de" + }, + { + "@value": "Fire Alarm", + "@language": "en" + }, + { + "@value": "سلاح ناري", + "@language": "ar" + }, + { + "@value": "Fire Alarm", + "@language": "ku" + }, + { + "@value": "Allarme antincendio", + "@language": "it" + }, + { + "@value": "Fire Alarm", + "@language": "sw" + }, + { + "@value": "Alarme de incêndio", + "@language": "pt" + }, + { + "@value": "Fire Alarm", + "@language": "oc" + }, + { + "@value": "Пожарная тревога", + "@language": "ru" + }, + { + "@value": "Fire Alarm", + "@language": "cy" + }, + { + "@value": "火災警報", + "@language": "ja" + }, + { + "@value": "Aláraim dóiteáin", + "@language": "ga" + }, + { + "@value": "फायर अलार्म", + "@language": "hi" + }, + { + "@value": "Fire Alarm", + "@language": "zh" + }, + { + "@value": "Fire Alarm", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#sensor", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#kettle", + "rdfs:label": [ + { + "@value": "Bouilloire", + "@language": "fr" + }, + { + "@value": "Pava", + "@language": "es" + }, + { + "@value": "Wasserkocher", + "@language": "de" + }, + { + "@value": "Kettle", + "@language": "en" + }, + { + "@value": "كيتل", + "@language": "ar" + }, + { + "@value": "Kettle", + "@language": "ku" + }, + { + "@value": "Bollitore", + "@language": "it" + }, + { + "@value": "Kettle", + "@language": "sw" + }, + { + "@value": "Kettle", + "@language": "pt" + }, + { + "@value": "Kettle", + "@language": "oc" + }, + { + "@value": "Чайник", + "@language": "ru" + }, + { + "@value": "Kettle", + "@language": "cy" + }, + { + "@value": "ケトル", + "@language": "ja" + }, + { + "@value": "bláthanna cumhra: cumhráin", + "@language": "ga" + }, + { + "@value": "केटल", + "@language": "hi" + }, + { + "@value": "凯图", + "@language": "zh" + }, + { + "@value": "Kettle", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#kitchen", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#coffee-machine", + "rdfs:label": [ + { + "@value": "Cafetière", + "@language": "fr" + }, + { + "@value": "Maquina de cafe", + "@language": "es" + }, + { + "@value": "Kaffeemaschine", + "@language": "de" + }, + { + "@value": "Coffee Machine", + "@language": "en" + }, + { + "@value": "آلة قهوة", + "@language": "ar" + }, + { + "@value": "Coffee Machine", + "@language": "ku" + }, + { + "@value": "Macchina da caffè", + "@language": "it" + }, + { + "@value": "Coffee Machine", + "@language": "sw" + }, + { + "@value": "Máquina de café", + "@language": "pt" + }, + { + "@value": "Coffee Machine", + "@language": "oc" + }, + { + "@value": "Кофемашина", + "@language": "ru" + }, + { + "@value": "Coffee Machine", + "@language": "cy" + }, + { + "@value": "コーヒーマシン", + "@language": "ja" + }, + { + "@value": "Meaisín Caife", + "@language": "ga" + }, + { + "@value": "कॉफी मशीन", + "@language": "hi" + }, + { + "@value": "Coffee Machine", + "@language": "zh" + }, + { + "@value": "Coffee Machine", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#kitchen", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#electric-heater", + "rdfs:label": [ + { + "@value": "Chauffage électrique", + "@language": "fr" + }, + { + "@value": "Encabezado eléctrico", + "@language": "es" + }, + { + "@value": "Elektrischer Header", + "@language": "de" + }, + { + "@value": "Electric Header", + "@language": "en" + }, + { + "@value": "رئيس كهربائي", + "@language": "ar" + }, + { + "@value": "Electric Header", + "@language": "ku" + }, + { + "@value": "Intestazione elettrica", + "@language": "it" + }, + { + "@value": "Electric Header", + "@language": "sw" + }, + { + "@value": "Cabeçalho elétrico", + "@language": "pt" + }, + { + "@value": "Electric Header", + "@language": "oc" + }, + { + "@value": "Электрический заголовок", + "@language": "ru" + }, + { + "@value": "Electric Header", + "@language": "cy" + }, + { + "@value": "電気ヘッダ", + "@language": "ja" + }, + { + "@value": "Leictreach Header", + "@language": "ga" + }, + { + "@value": "इलेक्ट्रिक हैडर", + "@language": "hi" + }, + { + "@value": "电力主任", + "@language": "zh" + }, + { + "@value": "Electric Header", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#heater", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#fire-extinguisher", + "rdfs:label": [ + { + "@value": "Extincteur d'incendie", + "@language": "fr" + }, + { + "@value": "Extintor de incendios", + "@language": "es" + }, + { + "@value": "Feuerlöscher", + "@language": "de" + }, + { + "@value": "Fire Extinguisher", + "@language": "en" + }, + { + "@value": "إطفاء الحريق", + "@language": "ar" + }, + { + "@value": "Fire Extinguisher", + "@language": "ku" + }, + { + "@value": "Estinguente del fuoco", + "@language": "it" + }, + { + "@value": "Fire Extinguisher", + "@language": "sw" + }, + { + "@value": "Extintor de incêndio", + "@language": "pt" + }, + { + "@value": "Fire Extinguisher", + "@language": "oc" + }, + { + "@value": "Огненный экстайзер", + "@language": "ru" + }, + { + "@value": "Fire Extinguisher", + "@language": "cy" + }, + { + "@value": "消火器", + "@language": "ja" + }, + { + "@value": "Seirbhís do Chustaiméirí", + "@language": "ga" + }, + { + "@value": "आग बुझाने की कल", + "@language": "hi" + }, + { + "@value": "消防队", + "@language": "zh" + }, + { + "@value": "Fire Extinguisher", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#safety", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#blow-torch", + "rdfs:label": [ + { + "@value": "Coup de flèche", + "@language": "fr" + }, + { + "@value": "Soplar antorcha", + "@language": "es" + }, + { + "@value": "Fackel", + "@language": "de" + }, + { + "@value": "Blow Torch", + "@language": "en" + }, + { + "@value": "بضائع", + "@language": "ar" + }, + { + "@value": "Blow Torch", + "@language": "ku" + }, + { + "@value": "Torcia del colpo", + "@language": "it" + }, + { + "@value": "Blow Torch", + "@language": "sw" + }, + { + "@value": "Tocha de sopro", + "@language": "pt" + }, + { + "@value": "Blow Torch", + "@language": "oc" + }, + { + "@value": "Blow факел", + "@language": "ru" + }, + { + "@value": "Blow Torch", + "@language": "cy" + }, + { + "@value": "ブロートーチ", + "@language": "ja" + }, + { + "@value": "Tóirse buille", + "@language": "ga" + }, + { + "@value": "झटका मशाल", + "@language": "hi" + }, + { + "@value": "Blow Toch", + "@language": "zh" + }, + { + "@value": "Blow Torch", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#heater", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#bunsen-burner", + "rdfs:label": [ + { + "@value": "Bec Bunsen", + "@language": "fr" + }, + { + "@value": "Bustsen Querador", + "@language": "es" + }, + { + "@value": "Bunsenbrenner", + "@language": "de" + }, + { + "@value": "Bunsen Burner", + "@language": "en" + }, + { + "@value": "Bunsen Burner", + "@language": "ar" + }, + { + "@value": "Bunsen Burner", + "@language": "ku" + }, + { + "@value": "Bunsen Burner", + "@language": "it" + }, + { + "@value": "Bunsen Burner", + "@language": "sw" + }, + { + "@value": "Bunsen Burner", + "@language": "pt" + }, + { + "@value": "Bunsen Burner", + "@language": "oc" + }, + { + "@value": "Бунсен Бурнер", + "@language": "ru" + }, + { + "@value": "Bunsen Burner", + "@language": "cy" + }, + { + "@value": "バーンセン・バーナー", + "@language": "ja" + }, + { + "@value": "Naisc ábhartha eile", + "@language": "ga" + }, + { + "@value": "बंसेन बर्नर", + "@language": "hi" + }, + { + "@value": "Bunsen Burner", + "@language": "zh" + }, + { + "@value": "Bunsen Burner", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#heater", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#oven", + "rdfs:label": [ + { + "@value": "Four", + "@language": "fr" + }, + { + "@value": "Horno", + "@language": "es" + }, + { + "@value": "Ofen", + "@language": "de" + }, + { + "@value": "Oven", + "@language": "en" + }, + { + "@value": "الفرن", + "@language": "ar" + }, + { + "@value": "Oven", + "@language": "ku" + }, + { + "@value": "Forno", + "@language": "it" + }, + { + "@value": "Oven", + "@language": "sw" + }, + { + "@value": "Forno de forno", + "@language": "pt" + }, + { + "@value": "Oven", + "@language": "oc" + }, + { + "@value": "ДА", + "@language": "ru" + }, + { + "@value": "Oven", + "@language": "cy" + }, + { + "@value": "オーブン", + "@language": "ja" + }, + { + "@value": "An tOileán", + "@language": "ga" + }, + { + "@value": "ओवन", + "@language": "hi" + }, + { + "@value": "Oven", + "@language": "zh" + }, + { + "@value": "Oven", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#heater", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#pliers", + "rdfs:label": [ + { + "@value": "Pinces", + "@language": "fr" + }, + { + "@value": "Alicates", + "@language": "es" + }, + { + "@value": "Zange", + "@language": "de" + }, + { + "@value": "Pliers", + "@language": "en" + }, + { + "@value": "البليز", + "@language": "ar" + }, + { + "@value": "Pliers", + "@language": "ku" + }, + { + "@value": "Pinze", + "@language": "it" + }, + { + "@value": "Pliers", + "@language": "sw" + }, + { + "@value": "Alicates", + "@language": "pt" + }, + { + "@value": "Pliers", + "@language": "oc" + }, + { + "@value": "Пели", + "@language": "ru" + }, + { + "@value": "Pliers", + "@language": "cy" + }, + { + "@value": "プライヤー", + "@language": "ja" + }, + { + "@value": "Púiríní", + "@language": "ga" + }, + { + "@value": "प्लायर", + "@language": "hi" + }, + { + "@value": "Pliers", + "@language": "zh" + }, + { + "@value": "Pliers", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#pliers", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#solder-sucker", + "rdfs:label": [ + { + "@value": "Ventouse de soudure", + "@language": "fr" + }, + { + "@value": "Lechón de soldadura", + "@language": "es" + }, + { + "@value": "Lötlutscher", + "@language": "de" + }, + { + "@value": "Solder Sucker", + "@language": "en" + }, + { + "@value": "سولدر سوكر", + "@language": "ar" + }, + { + "@value": "Solder Sucker", + "@language": "ku" + }, + { + "@value": "Solder Sucker", + "@language": "it" + }, + { + "@value": "Solder Sucker", + "@language": "sw" + }, + { + "@value": "Filho da mãe", + "@language": "pt" + }, + { + "@value": "Solder Sucker", + "@language": "oc" + }, + { + "@value": "Солдат Сукер", + "@language": "ru" + }, + { + "@value": "Solder Sucker", + "@language": "cy" + }, + { + "@value": "はんだサッカー", + "@language": "ja" + }, + { + "@value": "Solder Sucker", + "@language": "ga" + }, + { + "@value": "सोल्डर चूसने वाला", + "@language": "hi" + }, + { + "@value": "索莱·苏克", + "@language": "zh" + }, + { + "@value": "Solder Sucker", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#soldering", + "@type": "dfc-p:ProductType" + }, + { + "@id": "https://tools/data/toolTypes.rdf#mains-extension-cable", + "rdfs:label": [ + { + "@value": "Câble d'extension secteur", + "@language": "fr" + }, + { + "@value": "Cable de extensión de la red", + "@language": "es" + }, + { + "@value": "Netzverlängerungskabel", + "@language": "de" + }, + { + "@value": "Mains Extension Cable", + "@language": "en" + }, + { + "@value": "مجموعة مواد التمديد الرئيسية", + "@language": "ar" + }, + { + "@value": "Mains Extension Cable", + "@language": "ku" + }, + { + "@value": "Cavo di estensione del supporto", + "@language": "it" + }, + { + "@value": "Mains Extension Cable", + "@language": "sw" + }, + { + "@value": "Cabo de extensão da rede", + "@language": "pt" + }, + { + "@value": "Mains Extension Cable", + "@language": "oc" + }, + { + "@value": "Главное расширение кабель", + "@language": "ru" + }, + { + "@value": "Mains Extension Cable", + "@language": "cy" + }, + { + "@value": "主要な延長ケーブル", + "@language": "ja" + }, + { + "@value": "Mainsí Síneadh Cábla", + "@language": "ga" + }, + { + "@value": "मेन्स एक्सटेंशन केबल", + "@language": "hi" + }, + { + "@value": "延长可兑换率", + "@language": "zh" + }, + { + "@value": "Mains Extension Cable", + "@language": "ca" + } + ], + "dfc-p:specialize": "https://tools/data/toolTypes.rdf#cable", + "@type": "dfc-p:ProductType" + } + ] +} diff --git a/ontology/units.json b/ontology/units.json new file mode 100644 index 000000000..066fecd65 --- /dev/null +++ b/ontology/units.json @@ -0,0 +1,28 @@ +{ + "@context":{ + "dfc-p": "http://static.datafoodconsortium.org/ontologies/dfc_ProductGlossary.owl#", + "dfc-u":"http://static.datafoodconsortium.org/data/units.rdf#" + }, + "@graph":[ + { + "@id":"dfc-u:kg", + "@type":"dfc-p:Unit", + "rdfs:label":"kilogramme" + }, + { + "@id":"dfc-u:u", + "@type":"dfc-p:Unit", + "rdfs:label":"unité" + }, + { + "@id":"dfc-u:g", + "@type":"dfc-p:Unit", + "rdfs:label":"gramme" + }, + { + "@id":"dfc-u:l", + "@type":"dfc-p:Unit", + "rdfs:label":"litre" + } + ] +} diff --git a/outbox.py b/outbox.py index 25bc2d163..b8b6fb98d 100644 --- a/outbox.py +++ b/outbox.py @@ -16,6 +16,7 @@ from posts import outboxMessageCreateWrap from posts import savePostToBox from posts import sendToFollowersThread from posts import sendToNamedAddresses +from utils import getBaseContentFromPost from utils import hasObjectDict from utils import getLocalNetworkAddresses from utils import getFullDomain @@ -26,6 +27,7 @@ from utils import isFeaturedWriter from utils import loadJson from utils import saveJson from utils import acctDir +from utils import localActorUrl from blocking import isBlockedDomain from blocking import outboxBlock from blocking import outboxUndoBlock @@ -61,7 +63,10 @@ def _outboxPersonReceiveUpdate(recentPostsCache: {}, if not messageJson.get('type'): return - print("messageJson['type'] " + messageJson['type']) + if not isinstance(messageJson['type'], str): + if debug: + print('DEBUG: c2s actor update type is not a string') + return if messageJson['type'] != 'Update': return if not hasObjectDict(messageJson): @@ -72,6 +77,10 @@ def _outboxPersonReceiveUpdate(recentPostsCache: {}, if debug: print('DEBUG: c2s actor update - no type') return + if not isinstance(messageJson['object']['type'], str): + if debug: + print('DEBUG: c2s actor update object type is not a string') + return if messageJson['object']['type'] != 'Person': if debug: print('DEBUG: not a c2s actor update') @@ -88,17 +97,21 @@ def _outboxPersonReceiveUpdate(recentPostsCache: {}, if debug: print('DEBUG: c2s actor update has no id field') return - actor = \ - httpPrefix + '://' + getFullDomain(domain, port) + '/users/' + nickname + if not isinstance(messageJson['id'], str): + if debug: + print('DEBUG: c2s actor update id is not a string') + return + domainFull = getFullDomain(domain, port) + actor = localActorUrl(httpPrefix, nickname, domainFull) if len(messageJson['to']) != 1: if debug: print('DEBUG: c2s actor update - to does not contain one actor ' + - messageJson['to']) + str(messageJson['to'])) return if messageJson['to'][0] != actor: if debug: print('DEBUG: c2s actor update - to does not contain actor ' + - messageJson['to'] + ' ' + actor) + str(messageJson['to']) + ' ' + actor) return if not messageJson['id'].startswith(actor + '#updates/'): if debug: @@ -178,7 +191,10 @@ def postMessageToOutbox(session, translate: {}, YTReplacementDomain: str, showPublishedDateOnly: bool, allowLocalNetworkAccess: bool, - city: str) -> bool: + city: str, systemLanguage: str, + sharedItemsFederatedDomains: [], + sharedItemFederationTokens: {}, + lowBandwidth: bool) -> bool: """post is received by the outbox Client to server message post https://www.w3.org/TR/activitypub/#client-to-server-outbox-delivery @@ -201,9 +217,9 @@ def postMessageToOutbox(session, translate: {}, # check that the outgoing post doesn't contain any markup # which can be used to implement exploits if hasObjectDict(messageJson): - if messageJson['object'].get('content'): - if dangerousMarkup(messageJson['object']['content'], - allowLocalNetworkAccess): + contentStr = getBaseContentFromPost(messageJson, systemLanguage) + if contentStr: + if dangerousMarkup(contentStr, allowLocalNetworkAccess): print('POST to outbox contains dangerous markup: ' + str(messageJson)) return False @@ -264,7 +280,7 @@ def postMessageToOutbox(session, translate: {}, print('DEBUG: domain is blocked: ' + messageJson['actor']) return False # replace youtube, so that google gets less tracking data - replaceYouTube(messageJson, YTReplacementDomain) + replaceYouTube(messageJson, YTReplacementDomain, systemLanguage) # https://www.w3.org/TR/activitypub/#create-activity-outbox messageJson['object']['attributedTo'] = messageJson['actor'] if messageJson['object'].get('attachment'): @@ -378,7 +394,7 @@ def postMessageToOutbox(session, translate: {}, if messageJson['type'] in indexedActivities: indexes = [outboxName, "inbox"] selfActor = \ - httpPrefix + '://' + domainFull + '/users/' + postToNickname + localActorUrl(httpPrefix, postToNickname, domainFull) for boxNameIndex in indexes: if not boxNameIndex: continue @@ -390,7 +406,8 @@ def postMessageToOutbox(session, translate: {}, messageJson, translate, YTReplacementDomain, allowLocalNetworkAccess, - recentPostsCache, debug): + recentPostsCache, debug, systemLanguage, + domainFull, personCache): inboxUpdateIndex('tlmedia', baseDir, postToNickname + '@' + domain, savedFilename, debug) @@ -449,7 +466,9 @@ def postMessageToOutbox(session, translate: {}, cachedWebfingers, personCache, messageJson, debug, - version) + version, + sharedItemsFederatedDomains, + sharedItemFederationTokens) followersThreads.append(followersThread) if debug: @@ -535,9 +554,9 @@ def postMessageToOutbox(session, translate: {}, if debug: print('DEBUG: handle share uploads') - outboxShareUpload(baseDir, httpPrefix, - postToNickname, domain, - port, messageJson, debug, city) + outboxShareUpload(baseDir, httpPrefix, postToNickname, domain, + port, messageJson, debug, city, + systemLanguage, translate, lowBandwidth) if debug: print('DEBUG: handle undo share uploads') @@ -571,5 +590,7 @@ def postMessageToOutbox(session, translate: {}, cachedWebfingers, personCache, messageJson, debug, - version) + version, + sharedItemsFederatedDomains, + sharedItemFederationTokens) return True diff --git a/person.py b/person.py index 1ee5180cb..985286d8e 100644 --- a/person.py +++ b/person.py @@ -37,6 +37,7 @@ from roles import setRole from roles import setRolesFromList from roles import getActorRolesList from media import processMetaData +from utils import removeLineEndings from utils import removeDomainPort from utils import getStatusNumber from utils import getFullDomain @@ -50,8 +51,10 @@ from utils import getProtocolPrefixes from utils import hasUsersPath from utils import getImageExtensions from utils import isImageFile -from utils import getUserPaths from utils import acctDir +from utils import getUserPaths +from utils import getGroupPaths +from utils import localActorUrl from session import createSession from session import getJson from webfinger import webfingerHandle @@ -136,8 +139,8 @@ def setProfileImage(baseDir: str, httpPrefix: str, nickname: str, domain: str, if personJson: personJson[iconFilenameBase]['mediaType'] = mediaType personJson[iconFilenameBase]['url'] = \ - httpPrefix + '://' + fullDomain + '/users/' + \ - nickname + '/' + iconFilename + localActorUrl(httpPrefix, nickname, fullDomain) + \ + '/' + iconFilename saveJson(personJson, personFilename) cmd = \ @@ -226,13 +229,15 @@ def getDefaultPersonContext() -> str: def _createPersonBase(baseDir: str, nickname: str, domain: str, port: int, httpPrefix: str, saveToFile: bool, manualFollowerApproval: bool, - password: str = None) -> (str, str, {}, {}): + groupAccount: bool, + password: str) -> (str, str, {}, {}): """Returns the private key, public key, actor and webfinger endpoint """ privateKeyPem, publicKeyPem = generateRSAKey() webfingerEndpoint = \ createWebfingerEndpoint(nickname, domain, port, - httpPrefix, publicKeyPem) + httpPrefix, publicKeyPem, + groupAccount) if saveToFile: storeWebfingerEndpoint(nickname, domain, port, baseDir, webfingerEndpoint) @@ -242,10 +247,12 @@ def _createPersonBase(baseDir: str, nickname: str, domain: str, port: int, domain = getFullDomain(domain, port) personType = 'Person' + if groupAccount: + personType = 'Group' # Enable follower approval by default approveFollowers = manualFollowerApproval personName = nickname - personId = httpPrefix + '://' + domain + '/users/' + nickname + personId = localActorUrl(httpPrefix, nickname, domain) inboxStr = personId + '/inbox' personUrl = httpPrefix + '://' + domain + '/@' + personName if nickname == 'inbox': @@ -294,7 +301,7 @@ def _createPersonBase(baseDir: str, nickname: str, domain: str, port: int, 'followers': personId + '/followers', 'following': personId + '/following', 'tts': personId + '/speaker', - 'shares': personId + '/shares', + 'shares': personId + '/catalog', 'hasOccupation': [ { '@type': 'Occupation', @@ -396,6 +403,7 @@ def _createPersonBase(baseDir: str, nickname: str, domain: str, port: int, print(publicKeyPem, file=text_file) if password: + password = removeLineEndings(password) storeBasicCredentials(baseDir, nickname, password) return privateKeyPem, publicKeyPem, newPerson, webfingerEndpoint @@ -434,8 +442,8 @@ def createGroup(baseDir: str, nickname: str, domain: str, port: int, newPerson, webfingerEndpoint) = createPerson(baseDir, nickname, domain, port, httpPrefix, saveToFile, - False, password) - newPerson['type'] = 'Group' + False, password, True) + return privateKeyPem, publicKeyPem, newPerson, webfingerEndpoint @@ -456,7 +464,8 @@ def savePersonQrcode(baseDir: str, def createPerson(baseDir: str, nickname: str, domain: str, port: int, httpPrefix: str, saveToFile: bool, manualFollowerApproval: bool, - password: str = None) -> (str, str, {}, {}): + password: str, + groupAccount: bool = False) -> (str, str, {}, {}): """Returns the private key, public key, actor and webfinger endpoint """ if not validNickname(domain, nickname): @@ -482,6 +491,7 @@ def createPerson(baseDir: str, nickname: str, domain: str, port: int, httpPrefix, saveToFile, manualFollowerApproval, + groupAccount, password) if not getConfigParam(baseDir, 'admin'): if nickname != 'news': @@ -552,7 +562,7 @@ def createSharedInbox(baseDir: str, nickname: str, domain: str, port: int, """Generates the shared inbox """ return _createPersonBase(baseDir, nickname, domain, port, httpPrefix, - True, True, None) + True, True, False, None) def createNewsInbox(baseDir: str, domain: str, port: int, @@ -584,6 +594,11 @@ def personUpgradeActor(baseDir: str, personJson: {}, personJson['published'] = published updateActor = True + if personJson.get('shares'): + if personJson['shares'].endswith('/shares'): + personJson['shares'] = personJson['id'] + '/catalog' + updateActor = True + occupationName = '' if personJson.get('occupationName'): occupationName = personJson['occupationName'] @@ -746,6 +761,7 @@ def personBoxJson(recentPostsCache: {}, boxname != 'tlfeatures' and \ boxname != 'outbox' and boxname != 'moderation' and \ boxname != 'tlbookmarks' and boxname != 'bookmarks': + print('ERROR: personBoxJson invalid box name ' + boxname) return None if not '/' + boxname in path: @@ -1186,6 +1202,18 @@ def setPersonNotes(baseDir: str, nickname: str, domain: str, return True +def _detectUsersPath(url: str) -> str: + """Tries to detect the /users/ path + """ + if '/' not in url: + return '/users/' + usersPaths = getUserPaths() + for possibleUsersPath in usersPaths: + if possibleUsersPath in url: + return possibleUsersPath + return '/users/' + + def getActorJson(hostDomain: str, handle: str, http: bool, gnunet: bool, debug: bool, quiet: bool = False) -> ({}, {}): """Returns the actor json @@ -1193,21 +1221,29 @@ def getActorJson(hostDomain: str, handle: str, http: bool, gnunet: bool, if debug: print('getActorJson for ' + handle) originalActor = handle + groupAccount = False + + # try to determine the users path + detectedUsersPath = _detectUsersPath(handle) if '/@' in handle or \ - '/users/' in handle or \ + detectedUsersPath in handle or \ handle.startswith('http') or \ handle.startswith('hyper'): + groupPaths = getGroupPaths() + if detectedUsersPath in groupPaths: + groupAccount = True # format: https://domain/@nick originalHandle = handle if not hasUsersPath(originalHandle): if not quiet or debug: print('getActorJson: Expected actor format: ' + - 'https://domain/@nick or https://domain/users/nick') + 'https://domain/@nick or https://domain' + + detectedUsersPath + 'nick') return None, None prefixes = getProtocolPrefixes() for prefix in prefixes: handle = handle.replace(prefix, '') - handle = handle.replace('/@', '/users/') + handle = handle.replace('/@', detectedUsersPath) paths = getUserPaths() userPathFound = False for userPath in paths: @@ -1234,6 +1270,10 @@ def getActorJson(hostDomain: str, handle: str, http: bool, gnunet: bool, return None, None if handle.startswith('@'): handle = handle[1:] + elif handle.startswith('!'): + # handle for a group + handle = handle[1:] + groupAccount = True if '@' not in handle: if not quiet: print('getActorJsonSyntax: --actor nickname@domain') @@ -1265,7 +1305,8 @@ def getActorJson(hostDomain: str, handle: str, http: bool, gnunet: bool, handle = nickname + '@' + domain wfRequest = webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - None, __version__, debug) + None, __version__, debug, + groupAccount) if not wfRequest: if not quiet: print('getActorJson Unable to webfinger ' + handle) @@ -1282,7 +1323,8 @@ def getActorJson(hostDomain: str, handle: str, http: bool, gnunet: bool, personUrl = None if wfRequest.get('errors'): if not quiet or debug: - print('getActorJson wfRequest error: ' + str(wfRequest['errors'])) + print('getActorJson wfRequest error: ' + + str(wfRequest['errors'])) if hasUsersPath(handle): personUrl = originalActor else: diff --git a/pgp.py b/pgp.py index 619bad3d5..5abf6856d 100644 --- a/pgp.py +++ b/pgp.py @@ -15,6 +15,7 @@ from utils import containsPGPPublicKey from utils import isPGPEncrypted from utils import getFullDomain from utils import getStatusNumber +from utils import localActorUrl from webfinger import webfingerHandle from posts import getPersonBox from auth import createBasicAuthHeader @@ -489,7 +490,7 @@ def pgpPublicKeyUpload(baseDir: str, session, if debug: print('Actor for ' + handle + ' obtained') - actor = httpPrefix + '://' + domainFull + '/users/' + nickname + actor = localActorUrl(httpPrefix, nickname, domainFull) handle = actor.replace('/users/', '/@') # check that this looks like the correct actor @@ -547,7 +548,7 @@ def pgpPublicKeyUpload(baseDir: str, session, # lookup the inbox for the To handle wfRequest = \ webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - domain, __version__, debug) + domain, __version__, debug, False) if not wfRequest: if debug: print('DEBUG: pgp actor update webfinger failed for ' + diff --git a/posts.py b/posts.py index 757751e31..3593f7ed3 100644 --- a/posts.py +++ b/posts.py @@ -31,6 +31,9 @@ from session import postImage from webfinger import webfingerHandle from httpsig import createSignedHeader from siteactive import siteIsActive +from languages import understoodPostLanguage +from utils import hasGroupType +from utils import getBaseContentFromPost from utils import removeDomainPort from utils import getPortFromDomain from utils import hasObjectDict @@ -60,6 +63,7 @@ from utils import votesOnNewswireItem from utils import removeHtml from utils import dangerousMarkup from utils import acctDir +from utils import localActorUrl from media import attachMedia from media import replaceYouTube from content import limitRepeatedWords @@ -181,18 +185,28 @@ def getUserUrl(wfRequest: {}, sourceId: int = 0, debug: bool = False) -> str: def parseUserFeed(session, feedUrl: str, asHeader: {}, projectVersion: str, httpPrefix: str, - domain: str, depth=0) -> {}: + domain: str, debug: bool, depth: int = 0) -> []: if depth > 10: + if debug: + print('Maximum search depth reached') return None + if debug: + print('Getting user feed for ' + feedUrl) + print('User feed header ' + str(asHeader)) feedJson = getJson(session, feedUrl, asHeader, None, False, projectVersion, httpPrefix, domain) if not feedJson: + if debug: + print('No user feed was returned') return None + if debug: + print('User feed:') + pprint(feedJson) + if 'orderedItems' in feedJson: - for item in feedJson['orderedItems']: - yield item + return feedJson['orderedItems'] nextUrl = None if 'first' in feedJson: @@ -200,28 +214,61 @@ def parseUserFeed(session, feedUrl: str, asHeader: {}, elif 'next' in feedJson: nextUrl = feedJson['next'] + if debug: + print('User feed next url: ' + str(nextUrl)) + if nextUrl: if isinstance(nextUrl, str): if '?max_id=0' not in nextUrl: userFeed = \ parseUserFeed(session, nextUrl, asHeader, projectVersion, httpPrefix, - domain, depth + 1) + domain, debug, depth + 1) if userFeed: - for item in userFeed: - yield item + return userFeed elif isinstance(nextUrl, dict): userFeed = nextUrl if userFeed.get('orderedItems'): - for item in userFeed['orderedItems']: - yield item + return userFeed['orderedItems'] + return None + + +def _getPersonBoxActor(session, baseDir: str, actor: str, + profileStr: str, asHeader: {}, + debug: bool, projectVersion: str, + httpPrefix: str, domain: str, + personCache: {}) -> {}: + """Returns the actor json for the given actor url + """ + personJson = \ + getPersonFromCache(baseDir, actor, personCache, True) + if personJson: + return personJson + + if '/channel/' in actor or '/accounts/' in actor: + asHeader = { + 'Accept': 'application/ld+json; profile="' + profileStr + '"' + } + personJson = getJson(session, actor, asHeader, None, + debug, projectVersion, httpPrefix, domain) + if personJson: + return personJson + asHeader = { + 'Accept': 'application/ld+json; profile="' + profileStr + '"' + } + personJson = getJson(session, actor, asHeader, None, + debug, projectVersion, httpPrefix, domain) + if personJson: + return personJson + print('Unable to get actor for ' + actor) + return None def getPersonBox(baseDir: str, session, wfRequest: {}, personCache: {}, projectVersion: str, httpPrefix: str, nickname: str, domain: str, - boxName='inbox', + boxName: str = 'inbox', sourceId=0) -> (str, str, str, str, str, str, str, str): debug = False profileStr = 'https://www.w3.org/ns/activitystreams' @@ -232,7 +279,9 @@ def getPersonBox(baseDir: str, session, wfRequest: {}, print('No webfinger given') return None, None, None, None, None, None, None + # get the actor / personUrl if not wfRequest.get('errors'): + # get the actor url from webfinger links personUrl = getUserUrl(wfRequest, sourceId, debug) else: if nickname == 'dev': @@ -243,27 +292,22 @@ def getPersonBox(baseDir: str, session, wfRequest: {}, 'Accept': 'application/ld+json; profile="' + profileStr + '"' } else: - personUrl = httpPrefix + '://' + domain + '/users/' + nickname + # the final fallback is a mastodon style url + personUrl = localActorUrl(httpPrefix, nickname, domain) if not personUrl: return None, None, None, None, None, None, None + + # get the actor json from the url personJson = \ - getPersonFromCache(baseDir, personUrl, personCache, True) + _getPersonBoxActor(session, baseDir, personUrl, + profileStr, asHeader, + debug, projectVersion, + httpPrefix, domain, + personCache) if not personJson: - if '/channel/' in personUrl or '/accounts/' in personUrl: - asHeader = { - 'Accept': 'application/ld+json; profile="' + profileStr + '"' - } - personJson = getJson(session, personUrl, asHeader, None, - debug, projectVersion, httpPrefix, domain) - if not personJson: - asHeader = { - 'Accept': 'application/ld+json; profile="' + profileStr + '"' - } - personJson = getJson(session, personUrl, asHeader, None, - debug, projectVersion, httpPrefix, domain) - if not personJson: - print('Unable to get actor for ' + personUrl) - return None, None, None, None, None, None, None + return None, None, None, None, None, None, None + + # get the url for the box/collection boxJson = None if not personJson.get(boxName): if personJson.get('endpoints'): @@ -271,7 +315,6 @@ def getPersonBox(baseDir: str, session, wfRequest: {}, boxJson = personJson['endpoints'][boxName] else: boxJson = personJson[boxName] - if not boxJson: return None, None, None, None, None, None, None @@ -322,9 +365,11 @@ def _getPosts(session, outboxUrl: str, maxPosts: int, personCache: {}, raw: bool, simple: bool, debug: bool, projectVersion: str, httpPrefix: str, - domain: str) -> {}: + domain: str, systemLanguage: str) -> {}: """Gets public posts from an outbox """ + if debug: + print('Getting outbox posts for ' + outboxUrl) personPosts = {} if not outboxUrl: return personPosts @@ -337,10 +382,12 @@ def _getPosts(session, outboxUrl: str, maxPosts: int, 'Accept': 'application/ld+json; profile="' + profileStr + '"' } if raw: + if debug: + print('Returning the raw feed') result = [] i = 0 userFeed = parseUserFeed(session, outboxUrl, asHeader, - projectVersion, httpPrefix, domain) + projectVersion, httpPrefix, domain, debug) for item in userFeed: result.append(item) i += 1 @@ -349,9 +396,14 @@ def _getPosts(session, outboxUrl: str, maxPosts: int, pprint(result) return None - i = 0 + if debug: + print('Returning a human readable version of the feed') userFeed = parseUserFeed(session, outboxUrl, asHeader, - projectVersion, httpPrefix, domain) + projectVersion, httpPrefix, domain, debug) + if not userFeed: + return personPosts + + i = 0 for item in userFeed: if not item.get('id'): if debug: @@ -361,111 +413,124 @@ def _getPosts(session, outboxUrl: str, maxPosts: int, if debug: print('No type') continue - if item['type'] != 'Create': + if item['type'] != 'Create' and item['type'] != 'Announce': if debug: print('Not Create type') continue - if not hasObjectDict(item): + if not isinstance(item, dict): if debug: print('item object is not a dict') + pprint(item) continue - if not item['object'].get('published'): - if debug: - print('No published attribute') - continue + if item.get('object'): + if isinstance(item['object'], dict): + if not item['object'].get('published'): + if debug: + print('No published attribute') + continue + elif isinstance(item['object'], str): + if not item.get('published'): + if debug: + print('No published attribute') + continue + else: + if debug: + print('object is not a dict or string') + continue if not personPosts.get(item['id']): # check that this is a public post # #Public should appear in the "to" list - if item['object'].get('to'): - isPublic = False - for recipient in item['object']['to']: - if recipient.endswith('#Public'): - isPublic = True - break - if not isPublic: - continue + if isinstance(item['object'], dict): + if item['object'].get('to'): + isPublic = False + for recipient in item['object']['to']: + if recipient.endswith('#Public'): + isPublic = True + break + if not isPublic: + continue + elif isinstance(item['object'], str): + if item.get('to'): + isPublic = False + for recipient in item['to']: + if recipient.endswith('#Public'): + isPublic = True + break + if not isPublic: + continue - content = \ - item['object']['content'].replace(''', "'") + content = getBaseContentFromPost(item, systemLanguage) + content = content.replace(''', "'") mentions = [] emoji = {} - if item['object'].get('tag'): - for tagItem in item['object']['tag']: - tagType = tagItem['type'].lower() - if tagType == 'emoji': - if tagItem.get('name') and tagItem.get('icon'): - if tagItem['icon'].get('url'): - # No emoji from non-permitted domains - if urlPermitted(tagItem['icon']['url'], - federationList, - "objects:read"): - emojiName = tagItem['name'] - emojiIcon = tagItem['icon']['url'] - emoji[emojiName] = emojiIcon + summary = '' + inReplyTo = '' + attachment = [] + sensitive = False + if isinstance(item['object'], dict): + if item['object'].get('tag'): + for tagItem in item['object']['tag']: + tagType = tagItem['type'].lower() + if tagType == 'emoji': + if tagItem.get('name') and tagItem.get('icon'): + if tagItem['icon'].get('url'): + # No emoji from non-permitted domains + if urlPermitted(tagItem['icon']['url'], + federationList): + emojiName = tagItem['name'] + emojiIcon = tagItem['icon']['url'] + emoji[emojiName] = emojiIcon + else: + if debug: + print('url not permitted ' + + tagItem['icon']['url']) + if tagType == 'mention': + if tagItem.get('name'): + if tagItem['name'] not in mentions: + mentions.append(tagItem['name']) + if len(mentions) > maxMentions: + if debug: + print('max mentions reached') + continue + if len(emoji) > maxEmoji: + if debug: + print('max emojis reached') + continue + + if item['object'].get('summary'): + if item['object']['summary']: + summary = item['object']['summary'] + + if item['object'].get('inReplyTo'): + if item['object']['inReplyTo']: + if isinstance(item['object']['inReplyTo'], str): + # No replies to non-permitted domains + if not urlPermitted(item['object']['inReplyTo'], + federationList): + if debug: + print('url not permitted ' + + item['object']['inReplyTo']) + continue + inReplyTo = item['object']['inReplyTo'] + + if item['object'].get('attachment'): + if item['object']['attachment']: + for attach in item['object']['attachment']: + if attach.get('name') and attach.get('url'): + # no attachments from non-permitted domains + if urlPermitted(attach['url'], + federationList): + attachment.append([attach['name'], + attach['url']]) else: if debug: print('url not permitted ' + - tagItem['icon']['url']) - if tagType == 'mention': - if tagItem.get('name'): - if tagItem['name'] not in mentions: - mentions.append(tagItem['name']) - if len(mentions) > maxMentions: - if debug: - print('max mentions reached') - continue - if len(emoji) > maxEmoji: - if debug: - print('max emojis reached') - continue + attach['url']) - summary = '' - if item['object'].get('summary'): - if item['object']['summary']: - summary = item['object']['summary'] - - inReplyTo = '' - if item['object'].get('inReplyTo'): - if item['object']['inReplyTo']: - if isinstance(item['object']['inReplyTo'], str): - # No replies to non-permitted domains - if not urlPermitted(item['object']['inReplyTo'], - federationList, - "objects:read"): - if debug: - print('url not permitted ' + - item['object']['inReplyTo']) - continue - inReplyTo = item['object']['inReplyTo'] - - conversation = '' - if item['object'].get('conversation'): - if item['object']['conversation']: - # no conversations originated in non-permitted domains - if urlPermitted(item['object']['conversation'], - federationList, "objects:read"): - conversation = item['object']['conversation'] - - attachment = [] - if item['object'].get('attachment'): - if item['object']['attachment']: - for attach in item['object']['attachment']: - if attach.get('name') and attach.get('url'): - # no attachments from non-permitted domains - if urlPermitted(attach['url'], - federationList, - "objects:read"): - attachment.append([attach['name'], - attach['url']]) - else: - if debug: - print('url not permitted ' + - attach['url']) - - sensitive = False - if item['object'].get('sensitive'): - sensitive = item['object']['sensitive'] + sensitive = False + if item['object'].get('sensitive'): + sensitive = item['object']['sensitive'] if simple: print(_cleanHtml(content) + '\n') @@ -479,8 +544,7 @@ def _getPosts(session, outboxUrl: str, maxPosts: int, "plaintext": _cleanHtml(content), "attachment": attachment, "mentions": mentions, - "emoji": emoji, - "conversation": conversation + "emoji": emoji } i += 1 @@ -489,24 +553,38 @@ def _getPosts(session, outboxUrl: str, maxPosts: int, return personPosts -def _updateWordFrequency(content: str, wordFrequency: {}) -> None: - """Creates a dictionary containing words and the number of times - that they appear +def _getCommonWords() -> str: + """Returns a list of common words """ - plainText = removeHtml(content) - removeChars = ('.', ';', '?') - for ch in removeChars: - plainText = plainText.replace(ch, ' ') - wordsList = plainText.split(' ') - commonWords = ( + return ( 'that', 'some', 'about', 'then', 'they', 'were', 'also', 'from', 'with', 'this', 'have', 'more', 'need', 'here', 'would', 'these', 'into', 'very', 'well', 'when', 'what', 'your', 'there', 'which', 'even', 'there', 'such', 'just', 'those', 'only', 'will', 'much', 'than', 'them', 'each', 'goes', - 'been', 'over', 'their', 'where', 'could', 'though' + 'been', 'over', 'their', 'where', 'could', 'though', + 'like', 'think', 'same', 'maybe', 'really', 'thing', + 'something', 'possible', 'actual', 'actually', + 'because', 'around', 'having', 'especially', 'other', + 'making', 'made', 'make', 'makes', 'including', + 'includes', 'know', 'knowing', 'knows', 'things', + 'say', 'says', 'saying', 'many', 'somewhat', + 'problem', 'problems', 'idea', 'ideas', + 'using', 'uses', 'https', 'still', 'want', 'wants' ) + + +def _updateWordFrequency(content: str, wordFrequency: {}) -> None: + """Creates a dictionary containing words and the number of times + that they appear + """ + plainText = removeHtml(content) + removeChars = ('.', ';', '?', '\n', ':') + for ch in removeChars: + plainText = plainText.replace(ch, ' ') + wordsList = plainText.split(' ') + commonWords = _getCommonWords() for word in wordsList: wordLen = len(word) if wordLen < 3: @@ -517,10 +595,10 @@ def _updateWordFrequency(content: str, wordFrequency: {}) -> None: if '&' in word or \ '"' in word or \ '@' in word or \ - '://' in word: + "'" in word or \ + "--" in word or \ + '//' in word: continue - if word.endswith(':'): - word = word.replace(':', '') if word.lower() in commonWords: continue if wordFrequency.get(word): @@ -538,7 +616,7 @@ def getPostDomains(session, outboxUrl: str, maxPosts: int, projectVersion: str, httpPrefix: str, domain: str, wordFrequency: {}, - domainList=[]) -> []: + domainList: [], systemLanguage: str) -> []: """Returns a list of domains referenced within public posts """ if not outboxUrl: @@ -556,16 +634,16 @@ def getPostDomains(session, outboxUrl: str, maxPosts: int, i = 0 userFeed = parseUserFeed(session, outboxUrl, asHeader, - projectVersion, httpPrefix, domain) + projectVersion, httpPrefix, domain, debug) for item in userFeed: i += 1 if i > maxPosts: break if not hasObjectDict(item): continue - if item['object'].get('content'): - _updateWordFrequency(item['object']['content'], - wordFrequency) + contentStr = getBaseContentFromPost(item, systemLanguage) + if contentStr: + _updateWordFrequency(contentStr, wordFrequency) if item['object'].get('inReplyTo'): if isinstance(item['object']['inReplyTo'], str): postDomain, postPort = \ @@ -611,7 +689,7 @@ def _getPostsForBlockedDomains(baseDir: str, i = 0 userFeed = parseUserFeed(session, outboxUrl, asHeader, - projectVersion, httpPrefix, domain) + projectVersion, httpPrefix, domain, debug) for item in userFeed: i += 1 if i > maxPosts: @@ -689,7 +767,7 @@ def savePostToBox(baseDir: str, httpPrefix: str, postId: str, if not postId: statusNumber, published = getStatusNumber() postId = \ - httpPrefix + '://' + originalDomain + '/users/' + nickname + \ + localActorUrl(httpPrefix, nickname, originalDomain) + \ '/statuses/' + statusNumber postJsonObject['id'] = postId + '/activity' if hasObjectDict(postJsonObject): @@ -838,17 +916,20 @@ def _createPostS2S(baseDir: str, nickname: str, domain: str, port: int, tags: [], attachImageFilename: str, mediaType: str, imageDescription: str, city: str, postObjectType: str, summary: str, - inReplyToAtomUri: str) -> {}: + inReplyToAtomUri: str, systemLanguage: str, + conversationId: str, lowBandwidth: bool) -> {}: """Creates a new server-to-server post """ - actorUrl = httpPrefix + '://' + domain + '/users/' + nickname + actorUrl = localActorUrl(httpPrefix, nickname, domain) idStr = \ - httpPrefix + '://' + domain + '/users/' + nickname + \ + localActorUrl(httpPrefix, nickname, domain) + \ '/statuses/' + statusNumber + '/replies' newPostUrl = \ httpPrefix + '://' + domain + '/@' + nickname + '/' + statusNumber newPostAttributedTo = \ - httpPrefix + '://' + domain + '/users/' + nickname + localActorUrl(httpPrefix, nickname, domain) + if not conversationId: + conversationId = newPostId newPost = { '@context': postContext, 'id': newPostId + '/activity', @@ -859,6 +940,7 @@ def _createPostS2S(baseDir: str, nickname: str, domain: str, port: int, 'cc': toCC, 'object': { 'id': newPostId, + 'conversation': conversationId, 'type': postObjectType, 'summary': summary, 'inReplyTo': inReplyTo, @@ -875,7 +957,7 @@ def _createPostS2S(baseDir: str, nickname: str, domain: str, port: int, 'mediaType': 'text/html', 'content': content, 'contentMap': { - 'en': content + systemLanguage: content }, 'attachment': [], 'tag': tags, @@ -894,7 +976,7 @@ def _createPostS2S(baseDir: str, nickname: str, domain: str, port: int, newPost['object'] = \ attachMedia(baseDir, httpPrefix, nickname, domain, port, newPost['object'], attachImageFilename, - mediaType, imageDescription, city) + mediaType, imageDescription, city, lowBandwidth) return newPost @@ -906,23 +988,28 @@ def _createPostC2S(baseDir: str, nickname: str, domain: str, port: int, tags: [], attachImageFilename: str, mediaType: str, imageDescription: str, city: str, postObjectType: str, summary: str, - inReplyToAtomUri: str) -> {}: + inReplyToAtomUri: str, systemLanguage: str, + conversationId: str, lowBandwidth: str) -> {}: """Creates a new client-to-server post """ + domainFull = getFullDomain(domain, port) idStr = \ - httpPrefix + '://' + domain + '/users/' + nickname + \ + localActorUrl(httpPrefix, nickname, domainFull) + \ '/statuses/' + statusNumber + '/replies' newPostUrl = \ httpPrefix + '://' + domain + '/@' + nickname + '/' + statusNumber + if not conversationId: + conversationId = newPostId newPost = { "@context": postContext, 'id': newPostId, + 'conversation': conversationId, 'type': postObjectType, 'summary': summary, 'inReplyTo': inReplyTo, 'published': published, 'url': newPostUrl, - 'attributedTo': httpPrefix + '://' + domain + '/users/' + nickname, + 'attributedTo': localActorUrl(httpPrefix, nickname, domainFull), 'to': toRecipients, 'cc': toCC, 'sensitive': sensitive, @@ -933,7 +1020,7 @@ def _createPostC2S(baseDir: str, nickname: str, domain: str, port: int, 'mediaType': 'text/html', 'content': content, 'contentMap': { - 'en': content + systemLanguage: content }, 'attachment': [], 'tag': tags, @@ -951,7 +1038,7 @@ def _createPostC2S(baseDir: str, nickname: str, domain: str, port: int, newPost = \ attachMedia(baseDir, httpPrefix, nickname, domain, port, newPost, attachImageFilename, - mediaType, imageDescription, city) + mediaType, imageDescription, city, lowBandwidth) return newPost @@ -1075,7 +1162,9 @@ def _createPostBase(baseDir: str, nickname: str, domain: str, port: int, maximumAttendeeCapacity: int, repliesModerationOption: str, anonymousParticipationEnabled: bool, - eventStatus: str, ticketUrl: str) -> {}: + eventStatus: str, ticketUrl: str, + systemLanguage: str, + conversationId: str, lowBandwidth: bool) -> {}: """Creates a message """ content = removeInvalidChars(content) @@ -1118,8 +1207,8 @@ def _createPostBase(baseDir: str, nickname: str, domain: str, port: int, statusNumber, published = getStatusNumber() newPostId = \ - httpPrefix + '://' + domain + '/users/' + \ - nickname + '/statuses/' + statusNumber + localActorUrl(httpPrefix, nickname, domain) + \ + '/statuses/' + statusNumber sensitive = False summary = None @@ -1191,8 +1280,6 @@ def _createPostBase(baseDir: str, nickname: str, domain: str, port: int, # the type of post to be made postObjectType = 'Note' - if eventUUID: - postObjectType = 'Event' if isArticle: postObjectType = 'Article' @@ -1206,7 +1293,8 @@ def _createPostBase(baseDir: str, nickname: str, domain: str, port: int, tags, attachImageFilename, mediaType, imageDescription, city, postObjectType, summary, - inReplyToAtomUri) + inReplyToAtomUri, systemLanguage, + conversationId, lowBandwidth) else: newPost = \ _createPostC2S(baseDir, nickname, domain, port, @@ -1217,7 +1305,8 @@ def _createPostBase(baseDir: str, nickname: str, domain: str, port: int, tags, attachImageFilename, mediaType, imageDescription, city, postObjectType, summary, - inReplyToAtomUri) + inReplyToAtomUri, systemLanguage, + conversationId, lowBandwidth) _createPostMentions(ccUrl, newPost, toRecipients, tags) @@ -1260,7 +1349,7 @@ def outboxMessageCreateWrap(httpPrefix: str, if messageJson.get('published'): published = messageJson['published'] newPostId = \ - httpPrefix + '://' + domain + '/users/' + nickname + \ + localActorUrl(httpPrefix, nickname, domain) + \ '/statuses/' + statusNumber cc = [] if messageJson.get('cc'): @@ -1269,7 +1358,7 @@ def outboxMessageCreateWrap(httpPrefix: str, "@context": "https://www.w3.org/ns/activitystreams", 'id': newPostId + '/activity', 'type': 'Create', - 'actor': httpPrefix + '://' + domain + '/users/' + nickname, + 'actor': localActorUrl(httpPrefix, nickname, domain), 'published': published, 'to': messageJson['to'], 'cc': cc, @@ -1279,7 +1368,7 @@ def outboxMessageCreateWrap(httpPrefix: str, newPost['object']['url'] = \ httpPrefix + '://' + domain + '/@' + nickname + '/' + statusNumber newPost['object']['atomUri'] = \ - httpPrefix + '://' + domain + '/users/' + nickname + \ + localActorUrl(httpPrefix, nickname, domain) + \ '/statuses/' + statusNumber return newPost @@ -1308,8 +1397,8 @@ def _postIsAddressedToFollowers(baseDir: str, if postJsonObject.get('cc'): ccList = postJsonObject['cc'] - followersUrl = httpPrefix + '://' + domainFull + '/users/' + \ - nickname + '/followers' + followersUrl = \ + localActorUrl(httpPrefix, nickname, domainFull) + '/followers' # does the followers url exist in 'to' or 'cc' lists? addressedToFollowers = False @@ -1341,13 +1430,13 @@ def undoPinnedPost(baseDir: str, nickname: str, domain: str) -> None: def getPinnedPostAsJson(baseDir: str, httpPrefix: str, nickname: str, domain: str, - domainFull: str) -> {}: + domainFull: str, systemLanguage: str) -> {}: """Returns the pinned profile post as json """ accountDir = acctDir(baseDir, nickname, domain) pinnedFilename = accountDir + '/pinToProfile.txt' pinnedPostJson = {} - actor = httpPrefix + '://' + domainFull + '/users/' + nickname + actor = localActorUrl(httpPrefix, nickname, domainFull) if os.path.isfile(pinnedFilename): pinnedContent = None with open(pinnedFilename, 'r') as pinFile: @@ -1362,7 +1451,7 @@ def getPinnedPostAsJson(baseDir: str, httpPrefix: str, ], 'content': pinnedContent, 'contentMap': { - 'en': pinnedContent + systemLanguage: pinnedContent }, 'id': actor + '/pinned', 'inReplyTo': None, @@ -1381,18 +1470,18 @@ def getPinnedPostAsJson(baseDir: str, httpPrefix: str, def jsonPinPost(baseDir: str, httpPrefix: str, nickname: str, domain: str, - domainFull: str) -> {}: + domainFull: str, systemLanguage: str) -> {}: """Returns a pinned post as json """ pinnedPostJson = \ getPinnedPostAsJson(baseDir, httpPrefix, nickname, domain, - domainFull) + domainFull, systemLanguage) itemsList = [] if pinnedPostJson: itemsList = [pinnedPostJson] - actor = httpPrefix + '://' + domainFull + '/users/' + nickname + actor = localActorUrl(httpPrefix, nickname, domainFull) return { '@context': [ 'https://www.w3.org/ns/activitystreams', @@ -1413,6 +1502,37 @@ def jsonPinPost(baseDir: str, httpPrefix: str, } +def regenerateIndexForBox(baseDir: str, + nickname: str, domain: str, boxName: str) -> None: + """Generates an index for the given box if it doesn't exist + Used by unit tests to artificially create an index + """ + boxDir = acctDir(baseDir, nickname, domain) + '/' + boxName + boxIndexFilename = boxDir + '.index' + + if not os.path.isdir(boxDir): + return + if os.path.isfile(boxIndexFilename): + return + + indexLines = [] + for subdir, dirs, files in os.walk(boxDir): + for f in files: + if ':##' not in f: + continue + indexLines.append(f) + break + + indexLines.sort(reverse=True) + + result = '' + with open(boxIndexFilename, 'w+') as fp: + for line in indexLines: + result += line + '\n' + fp.write(line + '\n') + print('Index generated for ' + boxName + '\n' + result) + + def createPublicPost(baseDir: str, nickname: str, domain: str, port: int, httpPrefix: str, content: str, followersOnly: bool, saveToFile: bool, @@ -1424,7 +1544,9 @@ def createPublicPost(baseDir: str, schedulePost: bool, eventDate: str, eventTime: str, location: str, - isArticle: bool) -> {}: + isArticle: bool, + systemLanguage: str, + conversationId: str, lowBandwidth: bool) -> {}: """Public post """ domainFull = getFullDomain(domain, port) @@ -1439,10 +1561,10 @@ def createPublicPost(baseDir: str, anonymousParticipationEnabled = None eventStatus = None ticketUrl = None + localActor = localActorUrl(httpPrefix, nickname, domainFull) return _createPostBase(baseDir, nickname, domain, port, 'https://www.w3.org/ns/activitystreams#Public', - httpPrefix + '://' + domainFull + '/users/' + - nickname + '/followers', + localActor + '/followers', httpPrefix, content, followersOnly, saveToFile, clientToServer, commentsEnabled, attachImageFilename, mediaType, @@ -1454,7 +1576,8 @@ def createPublicPost(baseDir: str, maximumAttendeeCapacity, repliesModerationOption, anonymousParticipationEnabled, - eventStatus, ticketUrl) + eventStatus, ticketUrl, systemLanguage, + conversationId, lowBandwidth) def _appendCitationsToBlogPost(baseDir: str, @@ -1496,7 +1619,8 @@ def createBlogPost(baseDir: str, inReplyTo: str, inReplyToAtomUri: str, subject: str, schedulePost: bool, eventDate: str, eventTime: str, - location: str) -> {}: + location: str, systemLanguage: str, + conversationId: str, lowBandwidth: bool) -> {}: blogJson = \ createPublicPost(baseDir, nickname, domain, port, httpPrefix, @@ -1506,7 +1630,9 @@ def createBlogPost(baseDir: str, imageDescription, city, inReplyTo, inReplyToAtomUri, subject, schedulePost, - eventDate, eventTime, location, True) + eventDate, eventTime, location, + True, systemLanguage, conversationId, + lowBandwidth) blogJson['object']['url'] = \ blogJson['object']['url'].replace('/@', '/users/') _appendCitationsToBlogPost(baseDir, nickname, domain, blogJson) @@ -1519,7 +1645,8 @@ def createNewsPost(baseDir: str, content: str, followersOnly: bool, saveToFile: bool, attachImageFilename: str, mediaType: str, imageDescription: str, city: str, - subject: str) -> {}: + subject: str, systemLanguage: str, + conversationId: str, lowBandwidth: bool) -> {}: clientToServer = False inReplyTo = None inReplyToAtomUri = None @@ -1536,7 +1663,9 @@ def createNewsPost(baseDir: str, imageDescription, city, inReplyTo, inReplyToAtomUri, subject, schedulePost, - eventDate, eventTime, location, True) + eventDate, eventTime, location, + True, systemLanguage, conversationId, + lowBandwidth) blog['object']['type'] = 'Article' return blog @@ -1548,15 +1677,16 @@ def createQuestionPost(baseDir: str, clientToServer: bool, commentsEnabled: bool, attachImageFilename: str, mediaType: str, imageDescription: str, city: str, - subject: str, durationDays: int) -> {}: + subject: str, durationDays: int, + systemLanguage: str, lowBandwidth: bool) -> {}: """Question post with multiple choice options """ domainFull = getFullDomain(domain, port) + localActor = localActorUrl(httpPrefix, nickname, domainFull) messageJson = \ _createPostBase(baseDir, nickname, domain, port, 'https://www.w3.org/ns/activitystreams#Public', - httpPrefix + '://' + domainFull + '/users/' + - nickname + '/followers', + localActor + '/followers', httpPrefix, content, followersOnly, saveToFile, clientToServer, commentsEnabled, attachImageFilename, mediaType, @@ -1564,7 +1694,8 @@ def createQuestionPost(baseDir: str, False, False, None, None, subject, False, None, None, None, None, None, None, None, None, - None, None, None, None, None) + None, None, None, None, None, systemLanguage, + None, lowBandwidth) messageJson['object']['type'] = 'Question' messageJson['object']['oneOf'] = [] messageJson['object']['votersCount'] = 0 @@ -1595,13 +1726,14 @@ def createUnlistedPost(baseDir: str, inReplyTo: str, inReplyToAtomUri: str, subject: str, schedulePost: bool, eventDate: str, eventTime: str, - location: str) -> {}: + location: str, systemLanguage: str, + conversationId: str, lowBandwidth: bool) -> {}: """Unlisted post. This has the #Public and followers links inverted. """ domainFull = getFullDomain(domain, port) + localActor = localActorUrl(httpPrefix, domainFull, nickname) return _createPostBase(baseDir, nickname, domain, port, - httpPrefix + '://' + domainFull + '/users/' + - nickname + '/followers', + localActor + '/followers', 'https://www.w3.org/ns/activitystreams#Public', httpPrefix, content, followersOnly, saveToFile, clientToServer, commentsEnabled, @@ -1611,7 +1743,8 @@ def createUnlistedPost(baseDir: str, inReplyTo, inReplyToAtomUri, subject, schedulePost, eventDate, eventTime, location, None, None, None, None, None, - None, None, None, None, None) + None, None, None, None, None, systemLanguage, + conversationId, lowBandwidth) def createFollowersOnlyPost(baseDir: str, @@ -1626,13 +1759,14 @@ def createFollowersOnlyPost(baseDir: str, inReplyToAtomUri: str, subject: str, schedulePost: bool, eventDate: str, eventTime: str, - location: str) -> {}: + location: str, systemLanguage: str, + conversationId: str, lowBandwidth: bool) -> {}: """Followers only post """ domainFull = getFullDomain(domain, port) + localActor = localActorUrl(httpPrefix, domainFull, nickname) return _createPostBase(baseDir, nickname, domain, port, - httpPrefix + '://' + domainFull + '/users/' + - nickname + '/followers', + localActor + '/followers', None, httpPrefix, content, followersOnly, saveToFile, clientToServer, commentsEnabled, @@ -1642,7 +1776,8 @@ def createFollowersOnlyPost(baseDir: str, inReplyTo, inReplyToAtomUri, subject, schedulePost, eventDate, eventTime, location, None, None, None, None, None, - None, None, None, None, None) + None, None, None, None, None, systemLanguage, + conversationId, lowBandwidth) def getMentionedPeople(baseDir: str, httpPrefix: str, @@ -1675,8 +1810,7 @@ def getMentionedPeople(baseDir: str, httpPrefix: str, if not validNickname(mentionedDomain, mentionedNickname): continue actor = \ - httpPrefix + '://' + handle.split('@')[1] + \ - '/users/' + mentionedNickname + localActorUrl(httpPrefix, mentionedNickname, handle.split('@')[1]) mentions.append(actor) return mentions @@ -1694,7 +1828,8 @@ def createDirectMessagePost(baseDir: str, subject: str, debug: bool, schedulePost: bool, eventDate: str, eventTime: str, - location: str) -> {}: + location: str, systemLanguage: str, + conversationId: str, lowBandwidth: bool) -> {}: """Direct Message post """ content = resolvePetnames(baseDir, nickname, domain, content) @@ -1717,7 +1852,8 @@ def createDirectMessagePost(baseDir: str, inReplyTo, inReplyToAtomUri, subject, schedulePost, eventDate, eventTime, location, None, None, None, None, None, - None, None, None, None, None) + None, None, None, None, None, systemLanguage, + conversationId, lowBandwidth) # mentioned recipients go into To rather than Cc messageJson['to'] = messageJson['object']['cc'] messageJson['object']['to'] = messageJson['to'] @@ -1735,7 +1871,8 @@ def createReportPost(baseDir: str, clientToServer: bool, commentsEnabled: bool, attachImageFilename: str, mediaType: str, imageDescription: str, city: str, - debug: bool, subject: str = None) -> {}: + debug: bool, subject: str, systemLanguage: str, + lowBandwidth: bool) -> {}: """Send a report to moderators """ domainFull = getFullDomain(domain, port) @@ -1762,8 +1899,9 @@ def createReportPost(baseDir: str, if line.startswith('@'): line = line[1:] if '@' in line: - moderatorActor = httpPrefix + '://' + domainFull + \ - '/users/' + line.split('@')[0] + nick = line.split('@')[0] + moderatorActor = \ + localActorUrl(httpPrefix, nick, domainFull) if moderatorActor not in moderatorsList: moderatorsList.append(moderatorActor) continue @@ -1774,16 +1912,16 @@ def createReportPost(baseDir: str, moderatorsList.append(line) else: if '/' not in line: - moderatorActor = httpPrefix + '://' + domainFull + \ - '/users/' + line + moderatorActor = \ + localActorUrl(httpPrefix, line, domainFull) if moderatorActor not in moderatorsList: moderatorsList.append(moderatorActor) if len(moderatorsList) == 0: # if there are no moderators then the admin becomes the moderator adminNickname = getConfigParam(baseDir, 'admin') if adminNickname: - moderatorsList.append(httpPrefix + '://' + domainFull + - '/users/' + adminNickname) + localActor = localActorUrl(httpPrefix, adminNickname, domainFull) + moderatorsList.append(localActor) if not moderatorsList: return None if debug: @@ -1807,7 +1945,8 @@ def createReportPost(baseDir: str, True, False, None, None, subject, False, None, None, None, None, None, None, None, None, - None, None, None, None, None) + None, None, None, None, None, systemLanguage, + None, lowBandwidth) if not postJsonObject: continue @@ -1864,8 +2003,12 @@ def threadSendPost(session, postJsonStr: str, federationList: [], if debug: # save the log file postLogFilename = baseDir + '/post.log' - with open(postLogFilename, 'a+') as logFile: - logFile.write(logStr + '\n') + if os.path.isfile(postLogFilename): + with open(postLogFilename, 'a+') as logFile: + logFile.write(logStr + '\n') + else: + with open(postLogFilename, 'w+') as logFile: + logFile.write(logStr + '\n') if postResult: if debug: @@ -1891,12 +2034,16 @@ def sendPost(projectVersion: str, imageDescription: str, city: str, federationList: [], sendThreads: [], postLog: [], cachedWebfingers: {}, personCache: {}, - isArticle: bool, + isArticle: bool, systemLanguage: str, + sharedItemsFederatedDomains: [], + sharedItemFederationTokens: {}, + lowBandwidth: bool, debug: bool = False, inReplyTo: str = None, inReplyToAtomUri: str = None, subject: str = None) -> int: - """Post to another inbox + """Post to another inbox. Used by unit tests. """ withDigest = True + conversationId = None if toNickname == 'inbox': # shared inbox actor on @domain@domain @@ -1909,7 +2056,7 @@ def sendPost(projectVersion: str, # lookup the inbox for the To handle wfRequest = webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - domain, projectVersion, debug) + domain, projectVersion, debug, False) if not wfRequest: return 1 if not isinstance(wfRequest, dict): @@ -1952,7 +2099,8 @@ def sendPost(projectVersion: str, inReplyToAtomUri, subject, False, None, None, None, None, None, None, None, None, - None, None, None, None, None) + None, None, None, None, None, systemLanguage, + conversationId, lowBandwidth) # get the senders private key privateKeyPem = _getPersonKey(nickname, domain, baseDir, 'private') @@ -1982,6 +2130,26 @@ def sendPost(projectVersion: str, toDomain, toPort, postPath, httpPrefix, withDigest, postJsonStr) + # if the "to" domain is within the shared items + # federation list then send the token for this domain + # so that it can request a catalog + if toDomain in sharedItemsFederatedDomains: + domainFull = getFullDomain(domain, port) + if sharedItemFederationTokens.get(domainFull): + signatureHeaderJson['Origin'] = domainFull + signatureHeaderJson['SharesCatalog'] = \ + sharedItemFederationTokens[domainFull] + if debug: + print('SharesCatalog added to header') + elif debug: + print(domainFull + ' not in sharedItemFederationTokens') + elif debug: + print(toDomain + ' not in sharedItemsFederatedDomains ' + + str(sharedItemsFederatedDomains)) + + if debug: + print('signatureHeaderJson: ' + str(signatureHeaderJson)) + # Keep the number of threads being used small while len(sendThreads) > 1000: print('WARN: Maximum threads reached - killing send thread') @@ -2011,9 +2179,12 @@ def sendPostViaServer(projectVersion: str, attachImageFilename: str, mediaType: str, imageDescription: str, city: str, cachedWebfingers: {}, personCache: {}, - isArticle: bool, debug: bool = False, + isArticle: bool, systemLanguage: str, + lowBandwidth: bool, + debug: bool = False, inReplyTo: str = None, inReplyToAtomUri: str = None, + conversationId: str = None, subject: str = None) -> int: """Send a post via a proxy (c2s) """ @@ -2021,14 +2192,14 @@ def sendPostViaServer(projectVersion: str, print('WARN: No session for sendPostViaServer') return 6 - fromDomain = getFullDomain(fromDomain, fromPort) + fromDomainFull = getFullDomain(fromDomain, fromPort) - handle = httpPrefix + '://' + fromDomain + '/@' + fromNickname + handle = httpPrefix + '://' + fromDomainFull + '/@' + fromNickname # lookup the inbox for the To handle wfRequest = \ webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - fromDomain, projectVersion, debug) + fromDomainFull, projectVersion, debug, False) if not wfRequest: if debug: print('DEBUG: post webfinger failed for ' + handle) @@ -2049,7 +2220,7 @@ def sendPostViaServer(projectVersion: str, personCache, projectVersion, httpPrefix, fromNickname, - fromDomain, postToBox, + fromDomainFull, postToBox, 82796) if not inboxUrl: if debug: @@ -2067,19 +2238,17 @@ def sendPostViaServer(projectVersion: str, clientToServer = True if toDomain.lower().endswith('public'): toPersonId = 'https://www.w3.org/ns/activitystreams#Public' - fromDomainFull = getFullDomain(fromDomain, fromPort) - cc = httpPrefix + '://' + fromDomainFull + '/users/' + \ - fromNickname + '/followers' + cc = localActorUrl(httpPrefix, fromNickname, fromDomainFull) + \ + '/followers' else: if toDomain.lower().endswith('followers') or \ toDomain.lower().endswith('followersonly'): toPersonId = \ - httpPrefix + '://' + \ - fromDomainFull + '/users/' + fromNickname + '/followers' + localActorUrl(httpPrefix, fromNickname, fromDomainFull) + \ + '/followers' else: toDomainFull = getFullDomain(toDomain, toPort) - toPersonId = httpPrefix + '://' + toDomainFull + \ - '/users/' + toNickname + toPersonId = localActorUrl(httpPrefix, toNickname, toDomainFull) postJsonObject = \ _createPostBase(baseDir, @@ -2093,13 +2262,14 @@ def sendPostViaServer(projectVersion: str, inReplyToAtomUri, subject, False, None, None, None, None, None, None, None, None, - None, None, None, None, None) + None, None, None, None, None, systemLanguage, + conversationId, lowBandwidth) authHeader = createBasicAuthHeader(fromNickname, password) if attachImageFilename: headers = { - 'host': fromDomain, + 'host': fromDomainFull, 'Authorization': authHeader } postResult = \ @@ -2111,7 +2281,7 @@ def sendPostViaServer(projectVersion: str, # return 9 headers = { - 'host': fromDomain, + 'host': fromDomainFull, 'Content-type': 'application/json', 'Authorization': authHeader } @@ -2189,7 +2359,8 @@ def sendSignedJson(postJsonObject: {}, session, baseDir: str, httpPrefix: str, saveToFile: bool, clientToServer: bool, federationList: [], sendThreads: [], postLog: [], cachedWebfingers: {}, - personCache: {}, debug: bool, projectVersion: str) -> int: + personCache: {}, debug: bool, projectVersion: str, + sharedItemsToken: str, groupAccount: bool) -> int: """Sends a signed json object to an inbox/outbox """ if debug: @@ -2202,11 +2373,9 @@ def sendSignedJson(postJsonObject: {}, session, baseDir: str, if toDomain.endswith('.onion') or toDomain.endswith('.i2p'): httpPrefix = 'http' -# sharedInbox = False if toNickname == 'inbox': # shared inbox actor on @domain@domain toNickname = toDomain -# sharedInbox = True toDomain = getFullDomain(toDomain, toPort) @@ -2227,7 +2396,7 @@ def sendSignedJson(postJsonObject: {}, session, baseDir: str, # lookup the inbox for the To handle wfRequest = webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - domain, projectVersion, debug) + domain, projectVersion, debug, groupAccount) if not wfRequest: if debug: print('DEBUG: webfinger for ' + handle + ' failed') @@ -2314,6 +2483,13 @@ def sendSignedJson(postJsonObject: {}, session, baseDir: str, createSignedHeader(privateKeyPem, nickname, domain, port, toDomain, toPort, postPath, httpPrefix, withDigest, postJsonStr) + # optionally add a token so that the receiving instance may access + # your shared items catalog + if sharedItemsToken: + signatureHeaderJson['Origin'] = getFullDomain(domain, port) + signatureHeaderJson['SharesCatalog'] = sharedItemsToken + elif debug: + print('Not sending shared items federation token') # Keep the number of threads being used small while len(sendThreads) > 1000: @@ -2424,7 +2600,9 @@ def sendToNamedAddresses(session, baseDir: str, sendThreads: [], postLog: [], cachedWebfingers: {}, personCache: {}, postJsonObject: {}, debug: bool, - projectVersion: str) -> None: + projectVersion: str, + sharedItemsFederatedDomains: [], + sharedItemFederationTokens: {}) -> None: """sends a post to the specific named addresses in to/cc """ if not session: @@ -2530,23 +2708,38 @@ def sendToNamedAddresses(session, baseDir: str, # another onion domain then switch the clearnet # domain for the onion one fromDomain = domain + fromDomainFull = getFullDomain(domain, port) fromHttpPrefix = httpPrefix if onionDomain: if toDomain.endswith('.onion'): fromDomain = onionDomain + fromDomainFull = onionDomain fromHttpPrefix = 'http' elif i2pDomain: if toDomain.endswith('.i2p'): fromDomain = i2pDomain + fromDomainFull = i2pDomain fromHttpPrefix = 'http' cc = [] + + # if the "to" domain is within the shared items + # federation list then send the token for this domain + # so that it can request a catalog + sharedItemsToken = None + if toDomain in sharedItemsFederatedDomains: + if sharedItemFederationTokens.get(fromDomainFull): + sharedItemsToken = sharedItemFederationTokens[fromDomainFull] + + groupAccount = hasGroupType(baseDir, address, personCache) + sendSignedJson(postJsonObject, session, baseDir, nickname, fromDomain, port, toNickname, toDomain, toPort, cc, fromHttpPrefix, True, clientToServer, federationList, sendThreads, postLog, cachedWebfingers, - personCache, debug, projectVersion) + personCache, debug, projectVersion, + sharedItemsToken, groupAccount) def _hasSharedInbox(session, httpPrefix: str, domain: str, @@ -2554,13 +2747,13 @@ def _hasSharedInbox(session, httpPrefix: str, domain: str, """Returns true if the given domain has a shared inbox This tries the new and the old way of webfingering the shared inbox """ - tryHandles = [ - domain + '@' + domain, - 'inbox@' + domain - ] + tryHandles = [] + if ':' not in domain: + tryHandles.append(domain + '@' + domain) + tryHandles.append('inbox@' + domain) for handle in tryHandles: wfRequest = webfingerHandle(session, handle, httpPrefix, {}, - None, __version__, debug) + None, __version__, debug, False) if wfRequest: if isinstance(wfRequest, dict): if not wfRequest.get('errors'): @@ -2594,7 +2787,9 @@ def sendToFollowers(session, baseDir: str, sendThreads: [], postLog: [], cachedWebfingers: {}, personCache: {}, postJsonObject: {}, debug: bool, - projectVersion: str) -> None: + projectVersion: str, + sharedItemsFederatedDomains: [], + sharedItemFederationTokens: {}) -> None: """sends a post to the followers of the given nickname """ print('sendToFollowers') @@ -2615,7 +2810,7 @@ def sendToFollowers(session, baseDir: str, print('Post to followers did not resolve any domains') return print('Post to followers resolved domains') - print(str(grouped)) + # print(str(grouped)) # this is after the message has arrived at the server clientToServer = False @@ -2634,6 +2829,15 @@ def sendToFollowers(session, baseDir: str, if debug: pprint(followerHandles) + # if the followers domain is within the shared items + # federation list then send the token for this domain + # so that it can request a catalog + sharedItemsToken = None + if followerDomain in sharedItemsFederatedDomains: + domainFull = getFullDomain(domain, port) + if sharedItemFederationTokens.get(domainFull): + sharedItemsToken = sharedItemFederationTokens[domainFull] + # check that the follower's domain is active followerDomainUrl = httpPrefix + '://' + followerDomain if not siteIsActive(followerDomainUrl): @@ -2677,6 +2881,11 @@ def sendToFollowers(session, baseDir: str, if withSharedInbox: toNickname = followerHandles[index].split('@')[0] + groupAccount = False + if toNickname.startswith('!'): + groupAccount = True + toNickname = toNickname[1:] + # if there are more than one followers on the domain # then send the post to the shared inbox if len(followerHandles) > 1: @@ -2698,13 +2907,19 @@ def sendToFollowers(session, baseDir: str, cc, fromHttpPrefix, True, clientToServer, federationList, sendThreads, postLog, cachedWebfingers, - personCache, debug, projectVersion) + personCache, debug, projectVersion, + sharedItemsToken, groupAccount) else: # send to individual followers without using a shared inbox for handle in followerHandles: print('Sending post to followers ' + handle) toNickname = handle.split('@')[0] + groupAccount = False + if toNickname.startswith('!'): + groupAccount = True + toNickname = toNickname[1:] + if postJsonObject['type'] != 'Update': print('Sending post to followers from ' + nickname + '@' + domain + ' to ' + @@ -2720,7 +2935,8 @@ def sendToFollowers(session, baseDir: str, cc, fromHttpPrefix, True, clientToServer, federationList, sendThreads, postLog, cachedWebfingers, - personCache, debug, projectVersion) + personCache, debug, projectVersion, + sharedItemsToken, groupAccount) time.sleep(4) @@ -2740,7 +2956,9 @@ def sendToFollowersThread(session, baseDir: str, sendThreads: [], postLog: [], cachedWebfingers: {}, personCache: {}, postJsonObject: {}, debug: bool, - projectVersion: str): + projectVersion: str, + sharedItemsFederatedDomains: [], + sharedItemFederationTokens: {}): """Returns a thread used to send a post to followers """ sendThread = \ @@ -2752,7 +2970,9 @@ def sendToFollowersThread(session, baseDir: str, sendThreads, postLog, cachedWebfingers, personCache, postJsonObject.copy(), debug, - projectVersion), daemon=True) + projectVersion, + sharedItemsFederatedDomains, + sharedItemFederationTokens), daemon=True) try: sendThread.start() except SocketError as e: @@ -2867,7 +3087,7 @@ def createModeration(baseDir: str, nickname: str, domain: str, port: int, pageNumber = 1 pageStr = '?page=' + str(pageNumber) - boxUrl = httpPrefix + '://' + domain + '/users/' + nickname + '/' + boxname + boxUrl = localActorUrl(httpPrefix, nickname, domain) + '/' + boxname boxHeader = { '@context': 'https://www.w3.org/ns/activitystreams', 'first': boxUrl + '?page=true', @@ -2926,7 +3146,9 @@ def isImageMedia(session, baseDir: str, httpPrefix: str, postJsonObject: {}, translate: {}, YTReplacementDomain: str, allowLocalNetworkAccess: bool, - recentPostsCache: {}, debug: bool) -> bool: + recentPostsCache: {}, debug: bool, + systemLanguage: str, + domainFull: str, personCache: {}) -> bool: """Returns true if the given post has attached image media """ if postJsonObject['type'] == 'Announce': @@ -2936,7 +3158,9 @@ def isImageMedia(session, baseDir: str, httpPrefix: str, __version__, translate, YTReplacementDomain, allowLocalNetworkAccess, - recentPostsCache, debug) + recentPostsCache, debug, + systemLanguage, + domainFull, personCache) if postJsonAnnounce: postJsonObject = postJsonAnnounce if postJsonObject['type'] != 'Create': @@ -3033,7 +3257,7 @@ def removePostInteractions(postJsonObject: {}, force: bool) -> bool: if not force: # If not authorized and it's a private post # then just don't show it within timelines - if not isPublicPost(postObj): + if not isPublicPost(postJsonObject): return False else: postObj = postJsonObject @@ -3121,6 +3345,7 @@ def _createBoxIndexed(recentPostsCache: {}, boxname != 'tlfeatures' and \ boxname != 'outbox' and boxname != 'tlbookmarks' and \ boxname != 'bookmarks': + print('ERROR: invalid boxname ' + boxname) return None # bookmarks and events timelines are like the inbox @@ -3135,9 +3360,10 @@ def _createBoxIndexed(recentPostsCache: {}, indexBoxName = boxname timelineNickname = 'news' + originalDomain = domain domain = getFullDomain(domain, port) - boxActor = httpPrefix + '://' + domain + '/users/' + nickname + boxActor = localActorUrl(httpPrefix, nickname, domain) pageStr = '?page=true' if pageNumber: @@ -3147,7 +3373,7 @@ def _createBoxIndexed(recentPostsCache: {}, pageStr = '?page=' + str(pageNumber) except BaseException: pass - boxUrl = httpPrefix + '://' + domain + '/users/' + nickname + '/' + boxname + boxUrl = localActorUrl(httpPrefix, nickname, domain) + '/' + boxname boxHeader = { '@context': 'https://www.w3.org/ns/activitystreams', 'first': boxUrl + '?page=true', @@ -3168,7 +3394,7 @@ def _createBoxIndexed(recentPostsCache: {}, postsInBox = [] indexFilename = \ - baseDir + '/accounts/' + timelineNickname + '@' + domain + \ + acctDir(baseDir, timelineNickname, originalDomain) + \ '/' + indexBoxName + '.index' totalPostsCount = 0 postsAddedToTimeline = 0 @@ -3216,11 +3442,13 @@ def _createBoxIndexed(recentPostsCache: {}, totalPostsCount += 1 postsAddedToTimeline += 1 continue + else: + print('Post not added to timeline') # read the post from file fullPostFilename = \ locatePost(baseDir, nickname, - domain, postUrl, False) + originalDomain, postUrl, False) if fullPostFilename: # has the post been rejected? if os.path.isfile(fullPostFilename + '.reject'): @@ -3239,7 +3467,7 @@ def _createBoxIndexed(recentPostsCache: {}, # if this is the features timeline fullPostFilename = \ locatePost(baseDir, timelineNickname, - domain, postUrl, False) + originalDomain, postUrl, False) if fullPostFilename: if _addPostToTimeline(fullPostFilename, boxname, postsInBox, boxActor): @@ -3266,8 +3494,8 @@ def _createBoxIndexed(recentPostsCache: {}, if lastPage < 1: lastPage = 1 boxHeader['last'] = \ - httpPrefix + '://' + domain + '/users/' + \ - nickname + '/' + boxname + '?page=' + str(lastPage) + localActorUrl(httpPrefix, nickname, domain) + \ + '/' + boxname + '?page=' + str(lastPage) if headerOnly: boxHeader['totalItems'] = len(postsInBox) @@ -3275,13 +3503,13 @@ def _createBoxIndexed(recentPostsCache: {}, if pageNumber > 1: prevPageStr = str(pageNumber - 1) boxHeader['prev'] = \ - httpPrefix + '://' + domain + '/users/' + \ - nickname + '/' + boxname + '?page=' + prevPageStr + localActorUrl(httpPrefix, nickname, domain) + \ + '/' + boxname + '?page=' + prevPageStr nextPageStr = str(pageNumber + 1) boxHeader['next'] = \ - httpPrefix + '://' + domain + '/users/' + \ - nickname + '/' + boxname + '?page=' + nextPageStr + localActorUrl(httpPrefix, nickname, domain) + \ + '/' + boxname + '?page=' + nextPageStr return boxHeader for postStr in postsInBox: @@ -3494,29 +3722,40 @@ def archivePostsForPerson(httpPrefix: str, nickname: str, domain: str, def getPublicPostsOfPerson(baseDir: str, nickname: str, domain: str, raw: bool, simple: bool, proxyType: str, port: int, httpPrefix: str, - debug: bool, projectVersion: str) -> None: + debug: bool, projectVersion: str, + systemLanguage: str) -> None: """ This is really just for test purposes """ print('Starting new session for getting public posts') session = createSession(proxyType) if not session: + if debug: + print('Session was not created') return personCache = {} cachedWebfingers = {} federationList = [] - + groupAccount = False + if nickname.startswith('!'): + nickname = nickname[1:] + groupAccount = True domainFull = getFullDomain(domain, port) handle = httpPrefix + "://" + domainFull + "/@" + nickname + wfRequest = \ webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - domain, projectVersion, debug) + domain, projectVersion, debug, groupAccount) if not wfRequest: + if debug: + print('No webfinger result was returned for ' + handle) sys.exit() if not isinstance(wfRequest, dict): print('Webfinger for ' + handle + ' did not return a dict. ' + str(wfRequest)) sys.exit() + if debug: + print('Getting the outbox for ' + handle) (personUrl, pubKeyId, pubKey, personId, shaedInbox, avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest, @@ -3524,19 +3763,23 @@ def getPublicPostsOfPerson(baseDir: str, nickname: str, domain: str, projectVersion, httpPrefix, nickname, domain, 'outbox', 62524) + if debug: + print('Actor url: ' + personId) + maxMentions = 10 maxEmoji = 10 maxAttachments = 5 _getPosts(session, personUrl, 30, maxMentions, maxEmoji, maxAttachments, federationList, personCache, raw, simple, debug, - projectVersion, httpPrefix, domain) + projectVersion, httpPrefix, domain, systemLanguage) def getPublicPostDomains(session, baseDir: str, nickname: str, domain: str, proxyType: str, port: int, httpPrefix: str, debug: bool, projectVersion: str, - wordFrequency: {}, domainList=[]) -> []: + wordFrequency: {}, domainList: [], + systemLanguage: str) -> []: """ Returns a list of domains referenced within public posts """ if not session: @@ -3551,7 +3794,7 @@ def getPublicPostDomains(session, baseDir: str, nickname: str, domain: str, handle = httpPrefix + "://" + domainFull + "/@" + nickname wfRequest = \ webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - domain, projectVersion, debug) + domain, projectVersion, debug, False) if not wfRequest: return domainList if not isinstance(wfRequest, dict): @@ -3574,7 +3817,7 @@ def getPublicPostDomains(session, baseDir: str, nickname: str, domain: str, maxAttachments, federationList, personCache, debug, projectVersion, httpPrefix, domain, - wordFrequency, domainList) + wordFrequency, domainList, systemLanguage) postDomains.sort() return postDomains @@ -3616,7 +3859,7 @@ def downloadFollowCollection(followType: str, def getPublicPostInfo(session, baseDir: str, nickname: str, domain: str, proxyType: str, port: int, httpPrefix: str, debug: bool, projectVersion: str, - wordFrequency: {}) -> []: + wordFrequency: {}, systemLanguage: str) -> []: """ Returns a dict of domains referenced within public posts """ if not session: @@ -3631,7 +3874,7 @@ def getPublicPostInfo(session, baseDir: str, nickname: str, domain: str, handle = httpPrefix + "://" + domainFull + "/@" + nickname wfRequest = \ webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - domain, projectVersion, debug) + domain, projectVersion, debug, False) if not wfRequest: return {} if not isinstance(wfRequest, dict): @@ -3655,7 +3898,7 @@ def getPublicPostInfo(session, baseDir: str, nickname: str, domain: str, maxAttachments, federationList, personCache, debug, projectVersion, httpPrefix, domain, - wordFrequency, []) + wordFrequency, [], systemLanguage) postDomains.sort() domainsInfo = {} for d in postDomains: @@ -3681,7 +3924,8 @@ def getPublicPostDomainsBlocked(session, baseDir: str, nickname: str, domain: str, proxyType: str, port: int, httpPrefix: str, debug: bool, projectVersion: str, - wordFrequency: {}, domainList=[]) -> []: + wordFrequency: {}, domainList: [], + systemLanguage: str) -> []: """ Returns a list of domains referenced within public posts which are globally blocked on this instance """ @@ -3689,7 +3933,7 @@ def getPublicPostDomainsBlocked(session, baseDir: str, getPublicPostDomains(session, baseDir, nickname, domain, proxyType, port, httpPrefix, debug, projectVersion, - wordFrequency, domainList) + wordFrequency, domainList, systemLanguage) if not postDomains: return [] @@ -3737,7 +3981,8 @@ def checkDomains(session, baseDir: str, nickname: str, domain: str, proxyType: str, port: int, httpPrefix: str, debug: bool, projectVersion: str, - maxBlockedDomains: int, singleCheck: bool) -> None: + maxBlockedDomains: int, singleCheck: bool, + systemLanguage: str) -> None: """Checks follower accounts for references to globally blocked domains """ wordFrequency = {} @@ -3765,7 +4010,8 @@ def checkDomains(session, baseDir: str, nonMutualDomain, proxyType, port, httpPrefix, debug, projectVersion, - wordFrequency, []) + wordFrequency, [], + systemLanguage) if blockedDomains: if len(blockedDomains) > maxBlockedDomains: followerWarningStr += handle + '\n' @@ -3785,7 +4031,8 @@ def checkDomains(session, baseDir: str, nonMutualDomain, proxyType, port, httpPrefix, debug, projectVersion, - wordFrequency, []) + wordFrequency, [], + systemLanguage) if blockedDomains: print(handle) for d in blockedDomains: @@ -3883,7 +4130,9 @@ def downloadAnnounce(session, baseDir: str, httpPrefix: str, postJsonObject: {}, projectVersion: str, translate: {}, YTReplacementDomain: str, allowLocalNetworkAccess: bool, - recentPostsCache: {}, debug: bool) -> {}: + recentPostsCache: {}, debug: bool, + systemLanguage: str, + domainFull: str, personCache: {}) -> {}: """Download the post referenced by an announce """ if not postJsonObject.get('object'): @@ -4011,7 +4260,11 @@ def downloadAnnounce(session, baseDir: str, httpPrefix: str, baseDir, nickname, domain, postId, recentPostsCache) return None - + if not understoodPostLanguage(baseDir, nickname, domain, + announcedJson, systemLanguage, + httpPrefix, domainFull, + personCache): + return None # Check the content of the announce contentStr = announcedJson['content'] if dangerousMarkup(contentStr, allowLocalNetworkAccess): @@ -4068,15 +4321,22 @@ def downloadAnnounce(session, baseDir: str, httpPrefix: str, recentPostsCache) return None postJsonObject = announcedJson - replaceYouTube(postJsonObject, YTReplacementDomain) + replaceYouTube(postJsonObject, YTReplacementDomain, systemLanguage) if saveJson(postJsonObject, announceFilename): return postJsonObject return None -def isMuted(baseDir: str, nickname: str, domain: str, postId: str) -> bool: +def isMuted(baseDir: str, nickname: str, domain: str, postId: str, + conversationId: str) -> bool: """Returns true if the given post is muted """ + if conversationId: + convMutedFilename = \ + acctDir(baseDir, nickname, domain) + '/conversation/' + \ + conversationId.replace('/', '#') + '.muted' + if os.path.isfile(convMutedFilename): + return True postFilename = locatePost(baseDir, nickname, domain, postId) if not postFilename: return False @@ -4099,11 +4359,10 @@ def sendBlockViaServer(baseDir: str, session, fromDomainFull = getFullDomain(fromDomain, fromPort) + blockActor = localActorUrl(httpPrefix, fromNickname, fromDomainFull) toUrl = 'https://www.w3.org/ns/activitystreams#Public' - ccUrl = httpPrefix + '://' + fromDomainFull + '/users/' + \ - fromNickname + '/followers' + ccUrl = blockActor + '/followers' - blockActor = httpPrefix + '://' + fromDomainFull + '/users/' + fromNickname newBlockJson = { "@context": "https://www.w3.org/ns/activitystreams", 'type': 'Block', @@ -4118,7 +4377,7 @@ def sendBlockViaServer(baseDir: str, session, # lookup the inbox for the To handle wfRequest = webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - fromDomain, projectVersion, debug) + fromDomain, projectVersion, debug, False) if not wfRequest: if debug: print('DEBUG: block webfinger failed for ' + handle) @@ -4180,7 +4439,7 @@ def sendMuteViaServer(baseDir: str, session, fromDomainFull = getFullDomain(fromDomain, fromPort) - actor = httpPrefix + '://' + fromDomainFull + '/users/' + fromNickname + actor = localActorUrl(httpPrefix, fromNickname, fromDomainFull) handle = actor.replace('/users/', '/@') newMuteJson = { @@ -4194,7 +4453,7 @@ def sendMuteViaServer(baseDir: str, session, # lookup the inbox for the To handle wfRequest = webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - fromDomain, projectVersion, debug) + fromDomain, projectVersion, debug, False) if not wfRequest: if debug: print('DEBUG: mute webfinger failed for ' + handle) @@ -4256,7 +4515,7 @@ def sendUndoMuteViaServer(baseDir: str, session, fromDomainFull = getFullDomain(fromDomain, fromPort) - actor = httpPrefix + '://' + fromDomainFull + '/users/' + fromNickname + actor = localActorUrl(httpPrefix, fromNickname, fromDomainFull) handle = actor.replace('/users/', '/@') undoMuteJson = { @@ -4275,7 +4534,7 @@ def sendUndoMuteViaServer(baseDir: str, session, # lookup the inbox for the To handle wfRequest = webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - fromDomain, projectVersion, debug) + fromDomain, projectVersion, debug, False) if not wfRequest: if debug: print('DEBUG: undo mute webfinger failed for ' + handle) @@ -4338,11 +4597,10 @@ def sendUndoBlockViaServer(baseDir: str, session, fromDomainFull = getFullDomain(fromDomain, fromPort) + blockActor = localActorUrl(httpPrefix, fromNickname, fromDomainFull) toUrl = 'https://www.w3.org/ns/activitystreams#Public' - ccUrl = httpPrefix + '://' + fromDomainFull + '/users/' + \ - fromNickname + '/followers' + ccUrl = blockActor + '/followers' - blockActor = httpPrefix + '://' + fromDomainFull + '/users/' + fromNickname newBlockJson = { "@context": "https://www.w3.org/ns/activitystreams", 'type': 'Undo', @@ -4361,7 +4619,7 @@ def sendUndoBlockViaServer(baseDir: str, session, # lookup the inbox for the To handle wfRequest = webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - fromDomain, projectVersion, debug) + fromDomain, projectVersion, debug, False) if not wfRequest: if debug: print('DEBUG: unblock webfinger failed for ' + handle) @@ -4446,7 +4704,7 @@ def c2sBoxJson(baseDir: str, session, return None domainFull = getFullDomain(domain, port) - actor = httpPrefix + '://' + domainFull + '/users/' + nickname + actor = localActorUrl(httpPrefix, nickname, domainFull) authHeader = createBasicAuthHeader(nickname, password) diff --git a/pyjsonld.py b/pyjsonld.py index fec0a1e34..1a598c5d2 100644 --- a/pyjsonld.py +++ b/pyjsonld.py @@ -39,6 +39,7 @@ from collections import deque, namedtuple from numbers import Integral, Real from context import getApschemaV1_9 +from context import getApschemaV1_20 from context import getApschemaV1_21 from context import getLitepubV0_1 from context import getLitepubSocial @@ -408,6 +409,13 @@ def load_document(url): 'document': getApschemaV1_9() } return doc + elif url.endswith('/apschema/v1.20'): + doc = { + 'contextUrl': None, + 'documentUrl': url, + 'document': getApschemaV1_20() + } + return doc elif url.endswith('/apschema/v1.21'): doc = { 'contextUrl': None, diff --git a/schedule.py b/schedule.py index de3564d45..3395b47df 100644 --- a/schedule.py +++ b/schedule.py @@ -112,7 +112,10 @@ def _updatePostSchedule(baseDir: str, handle: str, httpd, httpd.YTReplacementDomain, httpd.showPublishedDateOnly, httpd.allowLocalNetworkAccess, - httpd.city): + httpd.city, httpd.systemLanguage, + httpd.sharedItemsFederatedDomains, + httpd.sharedItemFederationTokens, + httpd.lowBandwidth): indexLines.remove(line) os.remove(postFilename) continue diff --git a/scripts/epicyon-notification b/scripts/epicyon-notification index d2f2c6301..4b6205d63 100755 --- a/scripts/epicyon-notification +++ b/scripts/epicyon-notification @@ -279,6 +279,21 @@ function notifications { fi fi + # send notifications for new wanted items to XMPP/email users + epicyonWantedFile="$epicyonDir/.newWanted" + if [ -f "$epicyonWantedFile" ]; then + if ! grep -q "##sent##" "$epicyonWantedFile"; then + epicyonWantedMessage=$(notification_translate_text 'Wanted') + epicyonWantedFileContent=$(echo "$epicyonWantedMessage")" "$(cat "$epicyonWantedFile") + if [[ "$epicyonWantedFileContent" == *':'* ]]; then + epicyonWantedMessage="Epicyon: $epicyonWantedFileContent" + fi + sendNotification "$USERNAME" "Epicyon" "$epicyonWantedMessage" + echo "##sent##" > "$epicyonWantedFile" + chown ${PROJECT_NAME}:${PROJECT_NAME} "$epicyonWantedFile" + fi + fi + # send notifications for follow requests to XMPP/email users epicyonFollowFile="$epicyonDir/followrequests.txt" epicyonFollowNotificationsFile="$epicyonDir/follownotifications.txt" diff --git a/session.py b/session.py index b1f0b8d89..f0e60db0b 100644 --- a/session.py +++ b/session.py @@ -124,7 +124,8 @@ def getJson(session, url: str, headers: {}, params: {}, debug: bool, else: print('WARN: getJson url: ' + url + ' failed with error code ' + - str(result.status_code)) + str(result.status_code) + + ' headers: ' + str(sessionHeaders)) return result.json() except requests.exceptions.RequestException as e: sessionHeaders2 = sessionHeaders.copy() diff --git a/shares.py b/shares.py index 20605c12a..e4b7e4045 100644 --- a/shares.py +++ b/shares.py @@ -8,12 +8,23 @@ __status__ = "Production" __module_group__ = "Timeline" import os +import re +import secrets import time +import datetime +from random import randint +from pprint import pprint +from session import getJson from webfinger import webfingerHandle from auth import createBasicAuthHeader +from auth import constantTimeStringCheck from posts import getPersonBox from session import postJson from session import postImage +from session import createSession +from utils import dateStringToSeconds +from utils import dateSecondsToString +from utils import getConfigParam from utils import getFullDomain from utils import validNickname from utils import loadJson @@ -23,14 +34,74 @@ from utils import hasObjectDict from utils import removeDomainPort from utils import isAccountDir from utils import acctDir +from utils import isfloat +from utils import getCategoryTypes +from utils import getSharesFilesList +from utils import localActorUrl from media import processMetaData +from media import convertImageToLowBandwidth +from filters import isFilteredGlobally +from siteactive import siteIsActive +from content import getPriceFromString +from blocking import isBlocked -def getValidSharedItemID(displayName: str) -> str: +def _loadDfcIds(baseDir: str, systemLanguage: str, + productType: str) -> {}: + """Loads the product types ontology + This is used to add an id to shared items + """ + productTypesFilename = \ + baseDir + '/ontology/custom' + productType.title() + 'Types.json' + if not os.path.isfile(productTypesFilename): + productTypesFilename = \ + baseDir + '/ontology/' + productType + 'Types.json' + productTypes = loadJson(productTypesFilename) + if not productTypes: + print('Unable to load ontology: ' + productTypesFilename) + return None + if not productTypes.get('@graph'): + print('No @graph list within ontology') + return None + if len(productTypes['@graph']) == 0: + print('@graph list has no contents') + return None + if not productTypes['@graph'][0].get('rdfs:label'): + print('@graph list entry has no rdfs:label') + return None + languageExists = False + for label in productTypes['@graph'][0]['rdfs:label']: + if not label.get('@language'): + continue + if label['@language'] == systemLanguage: + languageExists = True + break + if not languageExists: + print('productTypes ontology does not contain the language ' + + systemLanguage) + return None + dfcIds = {} + for item in productTypes['@graph']: + if not item.get('@id'): + continue + if not item.get('rdfs:label'): + continue + for label in item['rdfs:label']: + if not label.get('@language'): + continue + if not label.get('@value'): + continue + if label['@language'] == systemLanguage: + dfcIds[label['@value'].lower()] = item['@id'] + break + return dfcIds + + +def _getValidSharedItemID(actor: str, displayName: str) -> str: """Removes any invalid characters from the display name to produce an item ID """ - removeChars = (' ', '\n', '\r') + removeChars = (' ', '\n', '\r', '#') for ch in removeChars: displayName = displayName.replace(ch, '') removeChars2 = ('+', '/', '\\', '?', '&') @@ -38,24 +109,31 @@ def getValidSharedItemID(displayName: str) -> str: displayName = displayName.replace(ch, '-') displayName = displayName.replace('.', '_') displayName = displayName.replace("’", "'") - return displayName + actor = actor.replace('://', '___') + actor = actor.replace('/', '--') + return actor + '--shareditems--' + displayName -def removeShare(baseDir: str, nickname: str, domain: str, - displayName: str) -> None: +def removeSharedItem(baseDir: str, nickname: str, domain: str, + itemID: str, + httpPrefix: str, domainFull: str, + sharesFileType: str) -> None: """Removes a share for a person """ - sharesFilename = acctDir(baseDir, nickname, domain) + '/shares.json' + sharesFilename = \ + acctDir(baseDir, nickname, domain) + '/' + sharesFileType + '.json' if not os.path.isfile(sharesFilename): - print('ERROR: missing shares.json ' + sharesFilename) + print('ERROR: remove shared item, missing ' + + sharesFileType + '.json ' + sharesFilename) return sharesJson = loadJson(sharesFilename) if not sharesJson: - print('ERROR: shares.json could not be loaded from ' + sharesFilename) + print('ERROR: remove shared item, ' + + sharesFileType + '.json could not be loaded from ' + + sharesFilename) return - itemID = getValidSharedItemID(displayName) if sharesJson.get(itemID): # remove any image for the item itemIDfile = baseDir + '/sharefiles/' + nickname + '/' + itemID @@ -73,7 +151,7 @@ def removeShare(baseDir: str, nickname: str, domain: str, '" does not exist in ' + sharesFilename) -def _addShareDurationSec(duration: str, published: str) -> int: +def _addShareDurationSec(duration: str, published: int) -> int: """Returns the duration for the shared item in seconds """ if ' ' not in duration: @@ -94,23 +172,151 @@ def _addShareDurationSec(duration: str, published: str) -> int: return 0 +def _dfcProductTypeFromCategory(baseDir: str, + itemCategory: str, translate: {}) -> str: + """Does the shared item category match a DFC product type? + If so then return the product type. + This will be used to select an appropriate ontology file + such as ontology/foodTypes.json + """ + productTypesList = getCategoryTypes(baseDir) + categoryLower = itemCategory.lower() + for productType in productTypesList: + if translate.get(productType): + if translate[productType] in categoryLower: + return productType + else: + if productType in categoryLower: + return productType + return None + + +def _getshareDfcId(baseDir: str, systemLanguage: str, + itemType: str, itemCategory: str, + translate: {}, dfcIds: {} = None) -> str: + """Attempts to obtain a DFC Id for the shared item, + based upon productTypes ontology. + See https://github.com/datafoodconsortium/ontology + """ + # does the category field match any prodyct type ontology + # files in the ontology subdirectory? + matchedProductType = \ + _dfcProductTypeFromCategory(baseDir, itemCategory, translate) + if not matchedProductType: + itemType = itemType.replace(' ', '_') + itemType = itemType.replace('.', '') + return 'epicyon#' + itemType + if not dfcIds: + dfcIds = _loadDfcIds(baseDir, systemLanguage, matchedProductType) + if not dfcIds: + return '' + itemTypeLower = itemType.lower() + matchName = '' + matchId = '' + for name, uri in dfcIds.items(): + if name not in itemTypeLower: + continue + if len(name) > len(matchName): + matchName = name + matchId = uri + if not matchId: + # bag of words match + maxMatchedWords = 0 + for name, uri in dfcIds.items(): + name = name.replace('-', ' ') + words = name.split(' ') + score = 0 + for wrd in words: + if wrd in itemTypeLower: + score += 1 + if score > maxMatchedWords: + maxMatchedWords = score + matchId = uri + return matchId + + +def _getshareTypeFromDfcId(dfcUri: str, dfcIds: {}) -> str: + """Attempts to obtain a share item type from its DFC Id, + based upon productTypes ontology. + See https://github.com/datafoodconsortium/ontology + """ + if dfcUri.startswith('epicyon#'): + itemType = dfcUri.split('#')[1] + itemType = itemType.replace('_', ' ') + return itemType + + for name, uri in dfcIds.items(): + if uri.endswith('#' + dfcUri): + return name + elif uri == dfcUri: + return name + return None + + +def _indicateNewShareAvailable(baseDir: str, httpPrefix: str, + nickname: str, domain: str, + domainFull: str, sharesFileType: str) -> None: + """Indicate to each account that a new share is available + """ + for subdir, dirs, files in os.walk(baseDir + '/accounts'): + for handle in dirs: + if not isAccountDir(handle): + continue + accountDir = baseDir + '/accounts/' + handle + if sharesFileType == 'shares': + newShareFile = accountDir + '/.newShare' + else: + newShareFile = accountDir + '/.newWanted' + if os.path.isfile(newShareFile): + continue + accountNickname = handle.split('@')[0] + # does this account block you? + if accountNickname != nickname: + if isBlocked(baseDir, accountNickname, domain, + nickname, domain, None): + continue + localActor = localActorUrl(httpPrefix, accountNickname, domainFull) + try: + with open(newShareFile, 'w+') as fp: + if sharesFileType == 'shares': + fp.write(localActor + '/tlshares') + else: + fp.write(localActor + '/tlwanted') + except BaseException: + pass + break + + def addShare(baseDir: str, httpPrefix: str, nickname: str, domain: str, port: int, displayName: str, summary: str, imageFilename: str, - itemType: str, itemCategory: str, location: str, - duration: str, debug: bool, city: str) -> None: + itemQty: float, itemType: str, itemCategory: str, location: str, + duration: str, debug: bool, city: str, + price: str, currency: str, + systemLanguage: str, translate: {}, + sharesFileType: str, lowBandwidth: bool) -> None: """Adds a new share """ - sharesFilename = acctDir(baseDir, nickname, domain) + '/shares.json' + if isFilteredGlobally(baseDir, + displayName + ' ' + summary + ' ' + + itemType + ' ' + itemCategory): + print('Shared item was filtered due to content') + return + sharesFilename = \ + acctDir(baseDir, nickname, domain) + '/' + sharesFileType + '.json' sharesJson = {} if os.path.isfile(sharesFilename): - sharesJson = loadJson(sharesFilename) + sharesJson = loadJson(sharesFilename, 1, 2) duration = duration.lower() published = int(time.time()) durationSec = _addShareDurationSec(duration, published) - itemID = getValidSharedItemID(displayName) + domainFull = getFullDomain(domain, port) + actor = localActorUrl(httpPrefix, nickname, domainFull) + itemID = _getValidSharedItemID(actor, displayName) + dfcId = _getshareDfcId(baseDir, systemLanguage, + itemType, itemCategory, translate) # has an image for this share been uploaded? imageUrl = None @@ -138,6 +344,8 @@ def addShare(baseDir: str, for ext in formats: if not imageFilename.endswith('.' + ext): continue + if lowBandwidth: + convertImageToLowBandwidth(imageFilename) processMetaData(baseDir, nickname, domain, imageFilename, itemIDfile + '.' + ext, city) @@ -151,31 +359,22 @@ def addShare(baseDir: str, "displayName": displayName, "summary": summary, "imageUrl": imageUrl, + "itemQty": float(itemQty), + "dfcId": dfcId, "itemType": itemType, "category": itemCategory, "location": location, "published": published, - "expire": durationSec + "expire": durationSec, + "itemPrice": price, + "itemCurrency": currency } saveJson(sharesJson, sharesFilename) - # indicate that a new share is available - for subdir, dirs, files in os.walk(baseDir + '/accounts'): - for handle in dirs: - if not isAccountDir(handle): - continue - accountDir = baseDir + '/accounts/' + handle - newShareFile = accountDir + '/.newShare' - if not os.path.isfile(newShareFile): - nickname = handle.split('@')[0] - try: - with open(newShareFile, 'w+') as fp: - fp.write(httpPrefix + '://' + domainFull + - '/users/' + nickname + '/tlshares') - except BaseException: - pass - break + _indicateNewShareAvailable(baseDir, httpPrefix, + nickname, domain, domainFull, + sharesFileType) def expireShares(baseDir: str) -> None: @@ -187,44 +386,51 @@ def expireShares(baseDir: str) -> None: continue nickname = account.split('@')[0] domain = account.split('@')[1] - _expireSharesForAccount(baseDir, nickname, domain) + for sharesFileType in getSharesFilesList(): + _expireSharesForAccount(baseDir, nickname, domain, + sharesFileType) break -def _expireSharesForAccount(baseDir: str, nickname: str, domain: str) -> None: +def _expireSharesForAccount(baseDir: str, nickname: str, domain: str, + sharesFileType: str) -> None: """Removes expired items from shares for a particular account """ handleDomain = removeDomainPort(domain) handle = nickname + '@' + handleDomain - sharesFilename = baseDir + '/accounts/' + handle + '/shares.json' - if os.path.isfile(sharesFilename): - sharesJson = loadJson(sharesFilename) - if sharesJson: - currTime = int(time.time()) - deleteItemID = [] - for itemID, item in sharesJson.items(): - if currTime > item['expire']: - deleteItemID.append(itemID) - if deleteItemID: - for itemID in deleteItemID: - del sharesJson[itemID] - # remove any associated images - itemIDfile = \ - baseDir + '/sharefiles/' + nickname + '/' + itemID - formats = getImageExtensions() - for ext in formats: - if os.path.isfile(itemIDfile + '.' + ext): - os.remove(itemIDfile + '.' + ext) - saveJson(sharesJson, sharesFilename) + sharesFilename = \ + baseDir + '/accounts/' + handle + '/' + sharesFileType + '.json' + if not os.path.isfile(sharesFilename): + return + sharesJson = loadJson(sharesFilename, 1, 2) + if not sharesJson: + return + currTime = int(time.time()) + deleteItemID = [] + for itemID, item in sharesJson.items(): + if currTime > item['expire']: + deleteItemID.append(itemID) + if not deleteItemID: + return + for itemID in deleteItemID: + del sharesJson[itemID] + # remove any associated images + itemIDfile = baseDir + '/sharefiles/' + nickname + '/' + itemID + formats = getImageExtensions() + for ext in formats: + if os.path.isfile(itemIDfile + '.' + ext): + os.remove(itemIDfile + '.' + ext) + saveJson(sharesJson, sharesFilename) def getSharesFeedForPerson(baseDir: str, domain: str, port: int, path: str, httpPrefix: str, - sharesPerPage=12) -> {}: + sharesFileType: str, + sharesPerPage: int = 12) -> {}: """Returns the shares for an account from GET requests """ - if '/shares' not in path: + if '/' + sharesFileType not in path: return None # handle page numbers headerOnly = True @@ -241,13 +447,15 @@ def getSharesFeedForPerson(baseDir: str, path = path.split('?page=')[0] headerOnly = False - if not path.endswith('/shares'): + if not path.endswith('/' + sharesFileType): return None nickname = None if path.startswith('/users/'): - nickname = path.replace('/users/', '', 1).replace('/shares', '') + nickname = \ + path.replace('/users/', '', 1).replace('/' + sharesFileType, '') if path.startswith('/@'): - nickname = path.replace('/@', '', 1).replace('/shares', '') + nickname = \ + path.replace('/@', '', 1).replace('/' + sharesFileType, '') if not nickname: return None if not validNickname(domain, nickname): @@ -256,7 +464,9 @@ def getSharesFeedForPerson(baseDir: str, domain = getFullDomain(domain, port) handleDomain = removeDomainPort(domain) - sharesFilename = acctDir(baseDir, nickname, handleDomain) + '/shares.json' + sharesFilename = \ + acctDir(baseDir, nickname, handleDomain) + '/' + \ + sharesFileType + '.json' if headerOnly: noOfShares = 0 @@ -264,11 +474,11 @@ def getSharesFeedForPerson(baseDir: str, sharesJson = loadJson(sharesFilename) if sharesJson: noOfShares = len(sharesJson.items()) - idStr = httpPrefix + '://' + domain + '/users/' + nickname + idStr = localActorUrl(httpPrefix, nickname, domain) shares = { '@context': 'https://www.w3.org/ns/activitystreams', - 'first': idStr + '/shares?page=1', - 'id': idStr + '/shares', + 'first': idStr + '/' + sharesFileType + '?page=1', + 'id': idStr + '/' + sharesFileType, 'totalItems': str(noOfShares), 'type': 'OrderedCollection' } @@ -278,18 +488,17 @@ def getSharesFeedForPerson(baseDir: str, pageNumber = 1 nextPageNumber = int(pageNumber + 1) - idStr = httpPrefix + '://' + domain + '/users/' + nickname + idStr = localActorUrl(httpPrefix, nickname, domain) shares = { '@context': 'https://www.w3.org/ns/activitystreams', - 'id': idStr + '/shares?page=' + str(pageNumber), + 'id': idStr + '/' + sharesFileType + '?page=' + str(pageNumber), 'orderedItems': [], - 'partOf': idStr + '/shares', + 'partOf': idStr + '/' + sharesFileType, 'totalItems': 0, 'type': 'OrderedCollectionPage' } if not os.path.isfile(sharesFilename): - print("test5") return shares currPage = 1 pageCtr = 0 @@ -301,6 +510,7 @@ def getSharesFeedForPerson(baseDir: str, pageCtr += 1 totalCtr += 1 if currPage == pageNumber: + item['shareId'] = itemID shares['orderedItems'].append(item) if pageCtr >= sharesPerPage: pageCtr = 0 @@ -311,8 +521,8 @@ def getSharesFeedForPerson(baseDir: str, lastPage = 1 if nextPageNumber > lastPage: shares['next'] = \ - httpPrefix + '://' + domain + '/users/' + nickname + \ - '/shares?page=' + str(lastPage) + localActorUrl(httpPrefix, nickname, domain) + \ + '/' + sharesFileType + '?page=' + str(lastPage) return shares @@ -321,23 +531,31 @@ def sendShareViaServer(baseDir, session, fromDomain: str, fromPort: int, httpPrefix: str, displayName: str, summary: str, imageFilename: str, - itemType: str, itemCategory: str, + itemQty: float, itemType: str, itemCategory: str, location: str, duration: str, cachedWebfingers: {}, personCache: {}, - debug: bool, projectVersion: str) -> {}: + debug: bool, projectVersion: str, + itemPrice: str, itemCurrency: str) -> {}: """Creates an item share via c2s """ if not session: print('WARN: No session for sendShareViaServer') return 6 + # convert $4.23 to 4.23 USD + newItemPrice, newItemCurrency = getPriceFromString(itemPrice) + if newItemPrice != itemPrice: + itemPrice = newItemPrice + if not itemCurrency: + if newItemCurrency != itemCurrency: + itemCurrency = newItemCurrency + fromDomainFull = getFullDomain(fromDomain, fromPort) + actor = localActorUrl(httpPrefix, fromNickname, fromDomainFull) toUrl = 'https://www.w3.org/ns/activitystreams#Public' - ccUrl = httpPrefix + '://' + fromDomainFull + \ - '/users/' + fromNickname + '/followers' + ccUrl = actor + '/followers' - actor = httpPrefix + '://' + fromDomainFull + '/users/' + fromNickname newShareJson = { "@context": "https://www.w3.org/ns/activitystreams", 'type': 'Add', @@ -347,10 +565,13 @@ def sendShareViaServer(baseDir, session, "type": "Offer", "displayName": displayName, "summary": summary, + "itemQty": float(itemQty), "itemType": itemType, "category": itemCategory, "location": location, "duration": duration, + "itemPrice": itemPrice, + "itemCurrency": itemCurrency, 'to': [toUrl], 'cc': [ccUrl] }, @@ -364,7 +585,7 @@ def sendShareViaServer(baseDir, session, wfRequest = \ webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - fromDomain, projectVersion, debug) + fromDomain, projectVersion, debug, False) if not wfRequest: if debug: print('DEBUG: share webfinger failed for ' + handle) @@ -440,11 +661,10 @@ def sendUndoShareViaServer(baseDir: str, session, fromDomainFull = getFullDomain(fromDomain, fromPort) + actor = localActorUrl(httpPrefix, fromNickname, fromDomainFull) toUrl = 'https://www.w3.org/ns/activitystreams#Public' - ccUrl = httpPrefix + '://' + fromDomainFull + \ - '/users/' + fromNickname + '/followers' + ccUrl = actor + '/followers' - actor = httpPrefix + '://' + fromDomainFull + '/users/' + fromNickname undoShareJson = { "@context": "https://www.w3.org/ns/activitystreams", 'type': 'Remove', @@ -465,7 +685,7 @@ def sendUndoShareViaServer(baseDir: str, session, # lookup the inbox for the To handle wfRequest = \ webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - fromDomain, projectVersion, debug) + fromDomain, projectVersion, debug, False) if not wfRequest: if debug: print('DEBUG: unshare webfinger failed for ' + handle) @@ -518,9 +738,258 @@ def sendUndoShareViaServer(baseDir: str, session, return undoShareJson +def sendWantedViaServer(baseDir, session, + fromNickname: str, password: str, + fromDomain: str, fromPort: int, + httpPrefix: str, displayName: str, + summary: str, imageFilename: str, + itemQty: float, itemType: str, itemCategory: str, + location: str, duration: str, + cachedWebfingers: {}, personCache: {}, + debug: bool, projectVersion: str, + itemMaxPrice: str, itemCurrency: str) -> {}: + """Creates a wanted item via c2s + """ + if not session: + print('WARN: No session for sendWantedViaServer') + return 6 + + # convert $4.23 to 4.23 USD + newItemMaxPrice, newItemCurrency = getPriceFromString(itemMaxPrice) + if newItemMaxPrice != itemMaxPrice: + itemMaxPrice = newItemMaxPrice + if not itemCurrency: + if newItemCurrency != itemCurrency: + itemCurrency = newItemCurrency + + fromDomainFull = getFullDomain(fromDomain, fromPort) + + actor = localActorUrl(httpPrefix, fromNickname, fromDomainFull) + toUrl = 'https://www.w3.org/ns/activitystreams#Public' + ccUrl = actor + '/followers' + + newShareJson = { + "@context": "https://www.w3.org/ns/activitystreams", + 'type': 'Add', + 'actor': actor, + 'target': actor + '/wanted', + 'object': { + "type": "Offer", + "displayName": displayName, + "summary": summary, + "itemQty": float(itemQty), + "itemType": itemType, + "category": itemCategory, + "location": location, + "duration": duration, + "itemPrice": itemMaxPrice, + "itemCurrency": itemCurrency, + 'to': [toUrl], + 'cc': [ccUrl] + }, + 'to': [toUrl], + 'cc': [ccUrl] + } + + handle = httpPrefix + '://' + fromDomainFull + '/@' + fromNickname + + # lookup the inbox for the To handle + wfRequest = \ + webfingerHandle(session, handle, httpPrefix, + cachedWebfingers, + fromDomain, projectVersion, debug, False) + if not wfRequest: + if debug: + print('DEBUG: share webfinger failed for ' + handle) + return 1 + if not isinstance(wfRequest, dict): + print('WARN: wanted webfinger for ' + handle + + ' did not return a dict. ' + str(wfRequest)) + return 1 + + postToBox = 'outbox' + + # get the actor inbox for the To handle + (inboxUrl, pubKeyId, pubKey, + fromPersonId, sharedInbox, + avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest, + personCache, projectVersion, + httpPrefix, fromNickname, + fromDomain, postToBox, + 83653) + + if not inboxUrl: + if debug: + print('DEBUG: wanted no ' + postToBox + + ' was found for ' + handle) + return 3 + if not fromPersonId: + if debug: + print('DEBUG: wanted no actor was found for ' + handle) + return 4 + + authHeader = createBasicAuthHeader(fromNickname, password) + + if imageFilename: + headers = { + 'host': fromDomain, + 'Authorization': authHeader + } + postResult = \ + postImage(session, imageFilename, [], + inboxUrl.replace('/' + postToBox, '/wanted'), + headers) + + headers = { + 'host': fromDomain, + 'Content-type': 'application/json', + 'Authorization': authHeader + } + postResult = \ + postJson(httpPrefix, fromDomainFull, + session, newShareJson, [], inboxUrl, headers, 30, True) + if not postResult: + if debug: + print('DEBUG: POST wanted failed for c2s to ' + inboxUrl) +# return 5 + + if debug: + print('DEBUG: c2s POST wanted item success') + + return newShareJson + + +def sendUndoWantedViaServer(baseDir: str, session, + fromNickname: str, password: str, + fromDomain: str, fromPort: int, + httpPrefix: str, displayName: str, + cachedWebfingers: {}, personCache: {}, + debug: bool, projectVersion: str) -> {}: + """Undoes a wanted item via c2s + """ + if not session: + print('WARN: No session for sendUndoWantedViaServer') + return 6 + + fromDomainFull = getFullDomain(fromDomain, fromPort) + + actor = localActorUrl(httpPrefix, fromNickname, fromDomainFull) + toUrl = 'https://www.w3.org/ns/activitystreams#Public' + ccUrl = actor + '/followers' + + undoShareJson = { + "@context": "https://www.w3.org/ns/activitystreams", + 'type': 'Remove', + 'actor': actor, + 'target': actor + '/wanted', + 'object': { + "type": "Offer", + "displayName": displayName, + 'to': [toUrl], + 'cc': [ccUrl] + }, + 'to': [toUrl], + 'cc': [ccUrl] + } + + handle = httpPrefix + '://' + fromDomainFull + '/@' + fromNickname + + # lookup the inbox for the To handle + wfRequest = \ + webfingerHandle(session, handle, httpPrefix, cachedWebfingers, + fromDomain, projectVersion, debug, False) + if not wfRequest: + if debug: + print('DEBUG: unwant webfinger failed for ' + handle) + return 1 + if not isinstance(wfRequest, dict): + print('WARN: unwant webfinger for ' + handle + + ' did not return a dict. ' + str(wfRequest)) + return 1 + + postToBox = 'outbox' + + # get the actor inbox for the To handle + (inboxUrl, pubKeyId, pubKey, + fromPersonId, sharedInbox, + avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest, + personCache, projectVersion, + httpPrefix, fromNickname, + fromDomain, postToBox, + 12663) + + if not inboxUrl: + if debug: + print('DEBUG: unwant no ' + postToBox + + ' was found for ' + handle) + return 3 + if not fromPersonId: + if debug: + print('DEBUG: unwant no actor was found for ' + handle) + return 4 + + authHeader = createBasicAuthHeader(fromNickname, password) + + headers = { + 'host': fromDomain, + 'Content-type': 'application/json', + 'Authorization': authHeader + } + postResult = \ + postJson(httpPrefix, fromDomainFull, + session, undoShareJson, [], inboxUrl, + headers, 30, True) + if not postResult: + if debug: + print('DEBUG: POST unwant failed for c2s to ' + inboxUrl) +# return 5 + + if debug: + print('DEBUG: c2s POST unwant success') + + return undoShareJson + + +def getSharedItemsCatalogViaServer(baseDir, session, + nickname: str, password: str, + domain: str, port: int, + httpPrefix: str, debug: bool) -> {}: + """Returns the shared items catalog via c2s + """ + if not session: + print('WARN: No session for getSharedItemsCatalogViaServer') + return 6 + + authHeader = createBasicAuthHeader(nickname, password) + + headers = { + 'host': domain, + 'Content-type': 'application/json', + 'Authorization': authHeader, + 'Accept': 'application/json' + } + domainFull = getFullDomain(domain, port) + url = localActorUrl(httpPrefix, nickname, domainFull) + '/catalog' + if debug: + print('Shared items catalog request to: ' + url) + catalogJson = getJson(session, url, headers, None, debug, + __version__, httpPrefix, None) + if not catalogJson: + if debug: + print('DEBUG: GET shared items catalog failed for c2s to ' + url) +# return 5 + + if debug: + print('DEBUG: c2s GET shared items catalog success') + + return catalogJson + + def outboxShareUpload(baseDir: str, httpPrefix: str, nickname: str, domain: str, port: int, - messageJson: {}, debug: bool, city: str) -> None: + messageJson: {}, debug: bool, city: str, + systemLanguage: str, translate: {}, + lowBandwidth: bool) -> None: """ When a shared item is received by the outbox from c2s """ if not messageJson.get('type'): @@ -545,6 +1014,10 @@ def outboxShareUpload(baseDir: str, httpPrefix: str, if debug: print('DEBUG: summary missing from Offer') return + if not messageJson['object'].get('itemQty'): + if debug: + print('DEBUG: itemQty missing from Offer') + return if not messageJson['object'].get('itemType'): if debug: print('DEBUG: itemType missing from Offer') @@ -553,24 +1026,36 @@ def outboxShareUpload(baseDir: str, httpPrefix: str, if debug: print('DEBUG: category missing from Offer') return - if not messageJson['object'].get('location'): - if debug: - print('DEBUG: location missing from Offer') - return if not messageJson['object'].get('duration'): if debug: print('DEBUG: duration missing from Offer') return + itemQty = float(messageJson['object']['itemQty']) + location = '' + if messageJson['object'].get('location'): + location = messageJson['object']['location'] + imageFilename = None + if messageJson['object'].get('imageFilename'): + imageFilename = messageJson['object']['imageFilename'] + if debug: + print('Adding shared item') + pprint(messageJson) + addShare(baseDir, httpPrefix, nickname, domain, port, messageJson['object']['displayName'], messageJson['object']['summary'], - messageJson['object']['imageFilename'], + imageFilename, + itemQty, messageJson['object']['itemType'], - messageJson['object']['itemCategory'], - messageJson['object']['location'], + messageJson['object']['category'], + location, messageJson['object']['duration'], - debug, city) + debug, city, + messageJson['object']['itemPrice'], + messageJson['object']['itemCurrency'], + systemLanguage, translate, 'shares', + lowBandwidth) if debug: print('DEBUG: shared item received via c2s') @@ -598,7 +1083,715 @@ def outboxUndoShareUpload(baseDir: str, httpPrefix: str, if debug: print('DEBUG: displayName missing from Offer') return - removeShare(baseDir, nickname, domain, - messageJson['object']['displayName']) + domainFull = getFullDomain(domain, port) + removeSharedItem(baseDir, nickname, domain, + messageJson['object']['displayName'], + httpPrefix, domainFull, 'shares') if debug: print('DEBUG: shared item removed via c2s') + + +def _sharesCatalogParams(path: str) -> (bool, float, float, str): + """Returns parameters when accessing the shares catalog + """ + today = False + minPrice = 0 + maxPrice = 9999999 + matchPattern = None + if '?' not in path: + return today, minPrice, maxPrice, matchPattern + args = path.split('?', 1)[1] + argList = args.split(';') + for arg in argList: + if '=' not in arg: + continue + key = arg.split('=')[0].lower() + value = arg.split('=')[1] + if key == 'today': + value = value.lower() + if 't' in value or 'y' in value or '1' in value: + today = True + elif key.startswith('min'): + if isfloat(value): + minPrice = float(value) + elif key.startswith('max'): + if isfloat(value): + maxPrice = float(value) + elif key.startswith('match'): + matchPattern = value + return today, minPrice, maxPrice, matchPattern + + +def sharesCatalogAccountEndpoint(baseDir: str, httpPrefix: str, + nickname: str, domain: str, + domainFull: str, + path: str, debug: bool, + sharesFileType: str) -> {}: + """Returns the endpoint for the shares catalog of a particular account + See https://github.com/datafoodconsortium/ontology + """ + today, minPrice, maxPrice, matchPattern = _sharesCatalogParams(path) + dfcUrl = \ + "http://static.datafoodconsortium.org/ontologies/DFC_FullModel.owl#" + dfcPtUrl = \ + "http://static.datafoodconsortium.org/data/productTypes.rdf#" + owner = localActorUrl(httpPrefix, nickname, domainFull) + if sharesFileType == 'shares': + dfcInstanceId = owner + '/catalog' + else: + dfcInstanceId = owner + '/wantedItems' + endpoint = { + "@context": { + "DFC": dfcUrl, + "dfc-pt": dfcPtUrl, + "@base": "http://maPlateformeNationale" + }, + "@id": dfcInstanceId, + "@type": "DFC:Entreprise", + "DFC:supplies": [] + } + + currDate = datetime.datetime.utcnow() + currDateStr = currDate.strftime("%Y-%m-%d") + + sharesFilename = \ + acctDir(baseDir, nickname, domain) + '/' + sharesFileType + '.json' + if not os.path.isfile(sharesFilename): + if debug: + print(sharesFileType + '.json file not found: ' + sharesFilename) + return endpoint + sharesJson = loadJson(sharesFilename, 1, 2) + if not sharesJson: + if debug: + print('Unable to load json for ' + sharesFilename) + return endpoint + + for itemID, item in sharesJson.items(): + if not item.get('dfcId'): + if debug: + print('Item does not have dfcId: ' + itemID) + continue + if '#' not in item['dfcId']: + continue + if today: + if not item['published'].startswith(currDateStr): + continue + if minPrice is not None: + if float(item['itemPrice']) < minPrice: + continue + if maxPrice is not None: + if float(item['itemPrice']) > maxPrice: + continue + description = item['displayName'] + ': ' + item['summary'] + if matchPattern: + if not re.match(matchPattern, description): + continue + + expireDate = datetime.datetime.fromtimestamp(item['expire']) + expireDateStr = expireDate.strftime("%Y-%m-%dT%H:%M:%SZ") + + shareId = _getValidSharedItemID(owner, item['displayName']) + if item['dfcId'].startswith('epicyon#'): + dfcId = "epicyon:" + item['dfcId'].split('#')[1] + else: + dfcId = "dfc-pt:" + item['dfcId'].split('#')[1] + priceStr = item['itemPrice'] + ' ' + item['itemCurrency'] + catalogItem = { + "@id": shareId, + "@type": "DFC:SuppliedProduct", + "DFC:hasType": dfcId, + "DFC:startDate": item['published'], + "DFC:expiryDate": expireDateStr, + "DFC:quantity": float(item['itemQty']), + "DFC:price": priceStr, + "DFC:Image": item['imageUrl'], + "DFC:description": description + } + endpoint['DFC:supplies'].append(catalogItem) + + return endpoint + + +def sharesCatalogEndpoint(baseDir: str, httpPrefix: str, + domainFull: str, + path: str, sharesFileType: str) -> {}: + """Returns the endpoint for the shares catalog for the instance + See https://github.com/datafoodconsortium/ontology + """ + today, minPrice, maxPrice, matchPattern = _sharesCatalogParams(path) + dfcUrl = \ + "http://static.datafoodconsortium.org/ontologies/DFC_FullModel.owl#" + dfcPtUrl = \ + "http://static.datafoodconsortium.org/data/productTypes.rdf#" + dfcInstanceId = httpPrefix + '://' + domainFull + '/catalog' + endpoint = { + "@context": { + "DFC": dfcUrl, + "dfc-pt": dfcPtUrl, + "@base": "http://maPlateformeNationale" + }, + "@id": dfcInstanceId, + "@type": "DFC:Entreprise", + "DFC:supplies": [] + } + + currDate = datetime.datetime.utcnow() + currDateStr = currDate.strftime("%Y-%m-%d") + + for subdir, dirs, files in os.walk(baseDir + '/accounts'): + for acct in dirs: + if not isAccountDir(acct): + continue + nickname = acct.split('@')[0] + domain = acct.split('@')[1] + owner = localActorUrl(httpPrefix, nickname, domainFull) + + sharesFilename = \ + acctDir(baseDir, nickname, domain) + '/' + \ + sharesFileType + '.json' + if not os.path.isfile(sharesFilename): + continue + print('Test 78363 ' + sharesFilename) + sharesJson = loadJson(sharesFilename, 1, 2) + if not sharesJson: + continue + + for itemID, item in sharesJson.items(): + if not item.get('dfcId'): + continue + if '#' not in item['dfcId']: + continue + if today: + if not item['published'].startswith(currDateStr): + continue + if minPrice is not None: + if float(item['itemPrice']) < minPrice: + continue + if maxPrice is not None: + if float(item['itemPrice']) > maxPrice: + continue + description = item['displayName'] + ': ' + item['summary'] + if matchPattern: + if not re.match(matchPattern, description): + continue + + startDateStr = dateSecondsToString(item['published']) + expireDateStr = dateSecondsToString(item['expire']) + shareId = _getValidSharedItemID(owner, item['displayName']) + if item['dfcId'].startswith('epicyon#'): + dfcId = "epicyon:" + item['dfcId'].split('#')[1] + else: + dfcId = "dfc-pt:" + item['dfcId'].split('#')[1] + priceStr = item['itemPrice'] + ' ' + item['itemCurrency'] + catalogItem = { + "@id": shareId, + "@type": "DFC:SuppliedProduct", + "DFC:hasType": dfcId, + "DFC:startDate": startDateStr, + "DFC:expiryDate": expireDateStr, + "DFC:quantity": float(item['itemQty']), + "DFC:price": priceStr, + "DFC:Image": item['imageUrl'], + "DFC:description": description + } + endpoint['DFC:supplies'].append(catalogItem) + + return endpoint + + +def sharesCatalogCSVEndpoint(baseDir: str, httpPrefix: str, + domainFull: str, + path: str, sharesFileType: str) -> str: + """Returns a CSV version of the shares catalog + """ + catalogJson = \ + sharesCatalogEndpoint(baseDir, httpPrefix, domainFull, path, + sharesFileType) + if not catalogJson: + return '' + if not catalogJson.get('DFC:supplies'): + return '' + csvStr = \ + 'id,type,hasType,startDate,expiryDate,' + \ + 'quantity,price,currency,Image,description,\n' + for item in catalogJson['DFC:supplies']: + csvStr += '"' + item['@id'] + '",' + csvStr += '"' + item['@type'] + '",' + csvStr += '"' + item['DFC:hasType'] + '",' + csvStr += '"' + item['DFC:startDate'] + '",' + csvStr += '"' + item['DFC:expiryDate'] + '",' + csvStr += str(item['DFC:quantity']) + ',' + csvStr += item['DFC:price'].split(' ')[0] + ',' + csvStr += '"' + item['DFC:price'].split(' ')[1] + '",' + csvStr += '"' + item['DFC:Image'] + '",' + description = item['DFC:description'].replace('"', "'") + csvStr += '"' + description + '",\n' + return csvStr + + +def generateSharedItemFederationTokens(sharedItemsFederatedDomains: [], + baseDir: str) -> {}: + """Generates tokens for shared item federated domains + """ + if not sharedItemsFederatedDomains: + return {} + + tokensJson = {} + if baseDir: + tokensFilename = \ + baseDir + '/accounts/sharedItemsFederationTokens.json' + if os.path.isfile(tokensFilename): + tokensJson = loadJson(tokensFilename, 1, 2) + if tokensJson is None: + tokensJson = {} + + tokensAdded = False + for domainFull in sharedItemsFederatedDomains: + if not tokensJson.get(domainFull): + tokensJson[domainFull] = '' + tokensAdded = True + + if not tokensAdded: + return tokensJson + if baseDir: + saveJson(tokensJson, tokensFilename) + return tokensJson + + +def updateSharedItemFederationToken(baseDir: str, + tokenDomainFull: str, newToken: str, + debug: bool, + tokensJson: {} = None) -> {}: + """Updates an individual token for shared item federation + """ + if debug: + print('Updating shared items token for ' + tokenDomainFull) + if not tokensJson: + tokensJson = {} + if baseDir: + tokensFilename = \ + baseDir + '/accounts/sharedItemsFederationTokens.json' + if os.path.isfile(tokensFilename): + if debug: + print('Update loading tokens for ' + tokenDomainFull) + tokensJson = loadJson(tokensFilename, 1, 2) + if tokensJson is None: + tokensJson = {} + updateRequired = False + if tokensJson.get(tokenDomainFull): + if tokensJson[tokenDomainFull] != newToken: + updateRequired = True + else: + updateRequired = True + if updateRequired: + tokensJson[tokenDomainFull] = newToken + if baseDir: + saveJson(tokensJson, tokensFilename) + return tokensJson + + +def mergeSharedItemTokens(baseDir: str, domainFull: str, + newSharedItemsFederatedDomains: [], + tokensJson: {}) -> {}: + """When the shared item federation domains list has changed, update + the tokens dict accordingly + """ + removals = [] + changed = False + for tokenDomainFull, tok in tokensJson.items(): + if domainFull: + if tokenDomainFull.startswith(domainFull): + continue + if tokenDomainFull not in newSharedItemsFederatedDomains: + removals.append(tokenDomainFull) + # remove domains no longer in the federation list + for tokenDomainFull in removals: + del tokensJson[tokenDomainFull] + changed = True + # add new domains from the federation list + for tokenDomainFull in newSharedItemsFederatedDomains: + if tokenDomainFull not in tokensJson: + tokensJson[tokenDomainFull] = '' + changed = True + if baseDir and changed: + tokensFilename = \ + baseDir + '/accounts/sharedItemsFederationTokens.json' + saveJson(tokensJson, tokensFilename) + return tokensJson + + +def createSharedItemFederationToken(baseDir: str, + tokenDomainFull: str, + force: bool, + tokensJson: {} = None) -> {}: + """Updates an individual token for shared item federation + """ + if not tokensJson: + tokensJson = {} + if baseDir: + tokensFilename = \ + baseDir + '/accounts/sharedItemsFederationTokens.json' + if os.path.isfile(tokensFilename): + tokensJson = loadJson(tokensFilename, 1, 2) + if tokensJson is None: + tokensJson = {} + if force or not tokensJson.get(tokenDomainFull): + tokensJson[tokenDomainFull] = secrets.token_urlsafe(64) + if baseDir: + saveJson(tokensJson, tokensFilename) + return tokensJson + + +def authorizeSharedItems(sharedItemsFederatedDomains: [], + baseDir: str, + originDomainFull: str, + callingDomainFull: str, + authHeader: str, + debug: bool, + tokensJson: {} = None) -> bool: + """HTTP simple token check for shared item federation + """ + if not sharedItemsFederatedDomains: + # no shared item federation + return False + if originDomainFull not in sharedItemsFederatedDomains: + if debug: + print(originDomainFull + + ' is not in the shared items federation list ' + + str(sharedItemsFederatedDomains)) + return False + if 'Basic ' in authHeader: + if debug: + print('DEBUG: shared item federation should not use basic auth') + return False + providedToken = authHeader.replace('\n', '').replace('\r', '').strip() + if not providedToken: + if debug: + print('DEBUG: shared item federation token is empty') + return False + if len(providedToken) < 60: + if debug: + print('DEBUG: shared item federation token is too small ' + + providedToken) + return False + if not tokensJson: + tokensFilename = \ + baseDir + '/accounts/sharedItemsFederationTokens.json' + if not os.path.isfile(tokensFilename): + if debug: + print('DEBUG: shared item federation tokens file missing ' + + tokensFilename) + return False + tokensJson = loadJson(tokensFilename, 1, 2) + if not tokensJson: + return False + if not tokensJson.get(callingDomainFull): + if debug: + print('DEBUG: shared item federation token ' + + 'check failed for ' + callingDomainFull) + return False + if not constantTimeStringCheck(tokensJson[callingDomainFull], + providedToken): + if debug: + print('DEBUG: shared item federation token ' + + 'mismatch for ' + callingDomainFull) + return False + return True + + +def _updateFederatedSharesCache(session, sharedItemsFederatedDomains: [], + baseDir: str, domainFull: str, + httpPrefix: str, + tokensJson: {}, debug: bool, + systemLanguage: str, + sharesFileType: str) -> None: + """Updates the cache of federated shares for the instance. + This enables shared items to be available even when other instances + might not be online + """ + # create directories where catalogs will be stored + cacheDir = baseDir + '/cache' + if not os.path.isdir(cacheDir): + os.mkdir(cacheDir) + if sharesFileType == 'shares': + catalogsDir = cacheDir + '/catalogs' + else: + catalogsDir = cacheDir + '/wantedItems' + if not os.path.isdir(catalogsDir): + os.mkdir(catalogsDir) + + asHeader = { + "Accept": "application/ld+json", + "Origin": domainFull + } + for federatedDomainFull in sharedItemsFederatedDomains: + # NOTE: federatedDomain does not have a port extension, + # so may not work in some situations + if federatedDomainFull.startswith(domainFull): + # only download from instances other than this one + continue + if not tokensJson.get(federatedDomainFull): + # token has been obtained for the other domain + continue + if not siteIsActive(httpPrefix + '://' + federatedDomainFull): + continue + if sharesFileType == 'shares': + url = httpPrefix + '://' + federatedDomainFull + '/catalog' + else: + url = httpPrefix + '://' + federatedDomainFull + '/wantedItems' + asHeader['Authorization'] = tokensJson[federatedDomainFull] + catalogJson = getJson(session, url, asHeader, None, + debug, __version__, httpPrefix, None) + if not catalogJson: + print('WARN: failed to download shared items catalog for ' + + federatedDomainFull) + continue + catalogFilename = catalogsDir + '/' + federatedDomainFull + '.json' + if saveJson(catalogJson, catalogFilename): + print('Downloaded shared items catalog for ' + federatedDomainFull) + sharesJson = _dfcToSharesFormat(catalogJson, + baseDir, systemLanguage) + if sharesJson: + sharesFilename = \ + catalogsDir + '/' + federatedDomainFull + '.' + \ + sharesFileType + '.json' + saveJson(sharesJson, sharesFilename) + print('Converted shares catalog for ' + federatedDomainFull) + else: + time.sleep(2) + + +def runFederatedSharesWatchdog(projectVersion: str, httpd) -> None: + """This tries to keep the federated shares update thread + running even if it dies + """ + print('Starting federated shares watchdog') + federatedSharesOriginal = \ + httpd.thrPostSchedule.clone(runFederatedSharesDaemon) + httpd.thrFederatedSharesDaemon.start() + while True: + time.sleep(55) + if httpd.thrFederatedSharesDaemon.is_alive(): + continue + httpd.thrFederatedSharesDaemon.kill() + httpd.thrFederatedSharesDaemon = \ + federatedSharesOriginal.clone(runFederatedSharesDaemon) + httpd.thrFederatedSharesDaemon.start() + print('Restarting federated shares daemon...') + + +def _generateNextSharesTokenUpdate(baseDir: str, + minDays: int, maxDays: int) -> None: + """Creates a file containing the next date when the shared items token + for this instance will be updated + """ + tokenUpdateDir = baseDir + '/accounts' + if not os.path.isdir(baseDir): + os.mkdir(baseDir) + if not os.path.isdir(tokenUpdateDir): + os.mkdir(tokenUpdateDir) + tokenUpdateFilename = tokenUpdateDir + '/.tokenUpdate' + nextUpdateSec = None + if os.path.isfile(tokenUpdateFilename): + with open(tokenUpdateFilename, 'r') as fp: + nextUpdateStr = fp.read() + if nextUpdateStr: + if nextUpdateStr.isdigit(): + nextUpdateSec = int(nextUpdateStr) + currTime = int(time.time()) + updated = False + if nextUpdateSec: + if currTime > nextUpdateSec: + nextUpdateDays = randint(minDays, maxDays) + nextUpdateInterval = int(60 * 60 * 24 * nextUpdateDays) + nextUpdateSec += nextUpdateInterval + updated = True + else: + nextUpdateDays = randint(minDays, maxDays) + nextUpdateInterval = int(60 * 60 * 24 * nextUpdateDays) + nextUpdateSec = currTime + nextUpdateInterval + updated = True + if updated: + with open(tokenUpdateFilename, 'w+') as fp: + fp.write(str(nextUpdateSec)) + + +def _regenerateSharesToken(baseDir: str, domainFull: str, + minDays: int, maxDays: int, httpd) -> None: + """Occasionally the shared items token for your instance is updated. + Scenario: + - You share items with $FriendlyInstance + - Some time later under new management + $FriendlyInstance becomes $HostileInstance + - You block $HostileInstance and remove them from your + federated shares domains list + - $HostileInstance still knows your shared items token, + and can still have access to your shared items if it presents a + spoofed Origin header together with the token + By rotating the token occasionally $HostileInstance will eventually + lose access to your federated shares. If other instances within your + federated shares list of domains continue to follow and communicate + then they will receive the new token automatically + """ + tokenUpdateFilename = baseDir + '/accounts/.tokenUpdate' + if not os.path.isfile(tokenUpdateFilename): + return + nextUpdateSec = None + with open(tokenUpdateFilename, 'r') as fp: + nextUpdateStr = fp.read() + if nextUpdateStr: + if nextUpdateStr.isdigit(): + nextUpdateSec = int(nextUpdateStr) + if not nextUpdateSec: + return + currTime = int(time.time()) + if currTime <= nextUpdateSec: + return + createSharedItemFederationToken(baseDir, domainFull, True, None) + _generateNextSharesTokenUpdate(baseDir, minDays, maxDays) + # update the tokens used within the daemon + httpd.sharedItemFederationTokens = \ + generateSharedItemFederationTokens(httpd.sharedItemsFederatedDomains, + baseDir) + + +def runFederatedSharesDaemon(baseDir: str, httpd, httpPrefix: str, + domainFull: str, proxyType: str, debug: bool, + systemLanguage: str) -> None: + """Runs the daemon used to update federated shared items + """ + secondsPerHour = 60 * 60 + fileCheckIntervalSec = 120 + time.sleep(60) + # the token for this instance will be changed every 7-14 days + minDays = 7 + maxDays = 14 + _generateNextSharesTokenUpdate(baseDir, minDays, maxDays) + while True: + sharedItemsFederatedDomainsStr = \ + getConfigParam(baseDir, 'sharedItemsFederatedDomains') + if not sharedItemsFederatedDomainsStr: + time.sleep(fileCheckIntervalSec) + continue + + # occasionally change the federated shared items token + # for this instance + _regenerateSharesToken(baseDir, domainFull, minDays, maxDays, httpd) + + # get a list of the domains within the shared items federation + sharedItemsFederatedDomains = [] + sharedItemsFederatedDomainsList = \ + sharedItemsFederatedDomainsStr.split(',') + for sharedFederatedDomain in sharedItemsFederatedDomainsList: + sharedItemsFederatedDomains.append(sharedFederatedDomain.strip()) + if not sharedItemsFederatedDomains: + time.sleep(fileCheckIntervalSec) + continue + + # load the tokens + tokensFilename = \ + baseDir + '/accounts/sharedItemsFederationTokens.json' + if not os.path.isfile(tokensFilename): + time.sleep(fileCheckIntervalSec) + continue + tokensJson = loadJson(tokensFilename, 1, 2) + if not tokensJson: + time.sleep(fileCheckIntervalSec) + continue + + session = createSession(proxyType) + for sharesFileType in getSharesFilesList(): + _updateFederatedSharesCache(session, sharedItemsFederatedDomains, + baseDir, domainFull, httpPrefix, + tokensJson, debug, systemLanguage, + sharesFileType) + time.sleep(secondsPerHour * 6) + + +def _dfcToSharesFormat(catalogJson: {}, + baseDir: str, systemLanguage: str) -> {}: + """Converts DFC format into the internal formal used to store shared items. + This simplifies subsequent search and display + """ + if not catalogJson.get('DFC:supplies'): + return {} + sharesJson = {} + + dfcIds = {} + productTypesList = getCategoryTypes(baseDir) + for productType in productTypesList: + dfcIds[productType] = _loadDfcIds(baseDir, systemLanguage, productType) + + currTime = int(time.time()) + for item in catalogJson['DFC:supplies']: + if not item.get('@id') or \ + not item.get('@type') or \ + not item.get('DFC:hasType') or \ + not item.get('DFC:startDate') or \ + not item.get('DFC:expiryDate') or \ + not item.get('DFC:quantity') or \ + not item.get('DFC:price') or \ + not item.get('DFC:Image') or \ + not item.get('DFC:description'): + continue + + if ' ' not in item['DFC:price']: + continue + if ':' not in item['DFC:description']: + continue + if ':' not in item['DFC:hasType']: + continue + + startTimeSec = dateStringToSeconds(item['DFC:startDate']) + if not startTimeSec: + continue + expiryTimeSec = dateStringToSeconds(item['DFC:expiryDate']) + if not expiryTimeSec: + continue + if expiryTimeSec < currTime: + # has expired + continue + + if item['DFC:hasType'].startswith('epicyon:'): + itemType = item['DFC:hasType'].split(':')[1] + itemType = itemType.replace('_', ' ') + itemCategory = 'non-food' + productType = None + else: + hasType = item['DFC:hasType'].split(':')[1] + itemType = None + productType = None + for prodType in productTypesList: + itemType = _getshareTypeFromDfcId(hasType, dfcIds[prodType]) + if itemType: + productType = prodType + break + itemCategory = 'food' + if not itemType: + continue + + allText = item['DFC:description'] + ' ' + itemType + ' ' + itemCategory + if isFilteredGlobally(baseDir, allText): + continue + + dfcId = None + if productType: + dfcId = dfcIds[productType][itemType] + itemID = item['@id'] + description = item['DFC:description'].split(':', 1)[1].strip() + + sharesJson[itemID] = { + "displayName": item['DFC:description'].split(':')[0], + "summary": description, + "imageUrl": item['DFC:Image'], + "itemQty": float(item['DFC:quantity']), + "dfcId": dfcId, + "itemType": itemType, + "category": itemCategory, + "location": "", + "published": startTimeSec, + "expire": expiryTimeSec, + "itemPrice": item['DFC:price'].split(' ')[0], + "itemCurrency": item['DFC:price'].split(' ')[1] + } + return sharesJson diff --git a/skills.py b/skills.py index 0c034a5a0..af8e125a4 100644 --- a/skills.py +++ b/skills.py @@ -19,6 +19,7 @@ from utils import loadJson from utils import getOccupationSkills from utils import setOccupationSkillsList from utils import acctDir +from utils import localActorUrl def setSkillsFromDict(actorJson: {}, skillsDict: {}) -> []: @@ -185,7 +186,7 @@ def sendSkillViaServer(baseDir: str, session, nickname: str, password: str, domainFull = getFullDomain(domain, port) - actor = httpPrefix + '://' + domainFull + '/users/' + nickname + actor = localActorUrl(httpPrefix, nickname, domainFull) toUrl = actor ccUrl = actor + '/followers' @@ -208,7 +209,7 @@ def sendSkillViaServer(baseDir: str, session, nickname: str, password: str, wfRequest = \ webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - domain, projectVersion, debug) + domain, projectVersion, debug, False) if not wfRequest: if debug: print('DEBUG: skill webfinger failed for ' + handle) diff --git a/socnet.py b/socnet.py index 4ae5d3e1c..cd2d4da72 100644 --- a/socnet.py +++ b/socnet.py @@ -17,7 +17,8 @@ from utils import getFullDomain def instancesGraph(baseDir: str, handles: str, proxyType: str, port: int, httpPrefix: str, - debug: bool, projectVersion: str) -> str: + debug: bool, projectVersion: str, + systemLanguage: str) -> str: """ Returns a dot graph of federating instances based upon a few sample handles. The handles argument should contain a comma separated list @@ -53,7 +54,7 @@ def instancesGraph(baseDir: str, handles: str, wfRequest = \ webfingerHandle(session, handle, httpPrefix, cachedWebfingers, - domain, projectVersion, debug) + domain, projectVersion, debug, False) if not wfRequest: return dotGraphStr + '}\n' if not isinstance(wfRequest, dict): @@ -74,7 +75,7 @@ def instancesGraph(baseDir: str, handles: str, maxAttachments, federationList, personCache, debug, projectVersion, httpPrefix, domain, - wordFrequency, []) + wordFrequency, [], systemLanguage) postDomains.sort() for fedDomain in postDomains: dotLineStr = ' "' + domain + '" -> "' + fedDomain + '";\n' diff --git a/speaker.py b/speaker.py index 2c413abd6..7d81a48e9 100644 --- a/speaker.py +++ b/speaker.py @@ -24,6 +24,7 @@ from utils import saveJson from utils import isPGPEncrypted from utils import hasObjectDict from utils import acctDir +from utils import localActorUrl from content import htmlReplaceQuoteMarks speakerRemoveChars = ('.\n', '. ', ',', ';', '?', '!') @@ -452,7 +453,7 @@ def _postToSpeakerJson(baseDir: str, httpPrefix: str, img['name'] + '. ' isDirect = isDM(postJsonObject) - actor = httpPrefix + '://' + domainFull + '/users/' + nickname + actor = localActorUrl(httpPrefix, nickname, domainFull) replyToYou = isReply(postJsonObject, actor) published = '' diff --git a/tests.py b/tests.py index 54f16e1e7..4064b1a8a 100644 --- a/tests.py +++ b/tests.py @@ -12,6 +12,7 @@ import os import shutil import json import datetime +from shutil import copyfile from random import randint from time import gmtime, strftime from pprint import pprint @@ -24,6 +25,8 @@ from cache import getPersonFromCache from threads import threadWithTrace from daemon import runDaemon from session import createSession +from session import getJson +from posts import regenerateIndexForBox from posts import removePostInteractions from posts import getMentionedPeople from posts import validContentWarning @@ -39,6 +42,15 @@ from follow import clearFollowers from follow import sendFollowRequestViaServer from follow import sendUnfollowRequestViaServer from siteactive import siteIsActive +from utils import isGroupAccount +from utils import getActorLanguagesList +from utils import getCategoryTypes +from utils import getSupportedLanguages +from utils import setConfigParam +from utils import isGroupActor +from utils import dateStringToSeconds +from utils import dateSecondsToString +from utils import validPassword from utils import userAgentDomain from utils import camelCaseSplit from utils import decodedHost @@ -66,6 +78,7 @@ from follow import unfollowAccount from follow import unfollowerOfAccount from follow import sendFollowRequest from person import createPerson +from person import createGroup from person import setDisplayNickname from person import setBio # from person import generateRSAKey @@ -95,6 +108,7 @@ from inbox import jsonPostAllowsComments from inbox import validInbox from inbox import validInboxFilenames from categories import guessHashtagCategory +from content import getPriceFromString from content import limitRepeatedWords from content import switchWords from content import extractTextFieldsInPOST @@ -123,10 +137,25 @@ from mastoapiv1 import getNicknameFromMastoApiV1Id from webapp_post import prepareHtmlPostNickname from speaker import speakerReplaceLinks from markdown import markdownToHtml +from languages import setActorLanguages +from languages import getActorLanguages +from languages import getLinksFromContent +from languages import addLinksToContent +from languages import libretranslate +from languages import libretranslateLanguages +from shares import authorizeSharedItems +from shares import generateSharedItemFederationTokens +from shares import createSharedItemFederationToken +from shares import updateSharedItemFederationToken +from shares import mergeSharedItemTokens +from shares import sendShareViaServer +from shares import getSharedItemsCatalogViaServer +testServerGroupRunning = False testServerAliceRunning = False testServerBobRunning = False testServerEveRunning = False +thrGroup = None thrAlice = None thrBob = None thrEve = None @@ -452,6 +481,8 @@ def createServerAlice(path: str, domain: str, port: int, shutil.rmtree(path) os.mkdir(path) os.chdir(path) + sharedItemsFederatedDomains = [] + systemLanguage = 'en' nickname = 'alice' httpPrefix = 'http' proxyType = None @@ -460,6 +491,7 @@ def createServerAlice(path: str, domain: str, port: int, domainMaxPostsPerDay = 1000 accountMaxPostsPerDay = 1000 allowDeletion = True + lowBandwidth = True privateKeyPem, publicKeyPem, person, wfEndpoint = \ createPerson(path, nickname, domain, port, httpPrefix, True, False, password) @@ -469,9 +501,9 @@ def createServerAlice(path: str, domain: str, port: int, assert setRole(path, nickname, domain, 'guru') if hasFollows: followPerson(path, nickname, domain, 'bob', bobAddress, - federationList, False) + federationList, False, False) followerOfPerson(path, nickname, domain, 'bob', bobAddress, - federationList, False) + federationList, False, False) if hasPosts: testFollowersOnly = False testSaveToFile = True @@ -489,6 +521,7 @@ def createServerAlice(path: str, domain: str, port: int, testEventTime = None testLocation = None testIsArticle = False + conversationId = None createPublicPost(path, nickname, domain, port, httpPrefix, "No wise fish would go anywhere without a porpoise", testFollowersOnly, @@ -501,7 +534,8 @@ def createServerAlice(path: str, domain: str, port: int, testInReplyTo, testInReplyToAtomUri, testSubject, testSchedulePost, testEventDate, testEventTime, testLocation, - testIsArticle) + testIsArticle, systemLanguage, conversationId, + lowBandwidth) createPublicPost(path, nickname, domain, port, httpPrefix, "Curiouser and curiouser!", testFollowersOnly, @@ -514,7 +548,8 @@ def createServerAlice(path: str, domain: str, port: int, testInReplyTo, testInReplyToAtomUri, testSubject, testSchedulePost, testEventDate, testEventTime, testLocation, - testIsArticle) + testIsArticle, systemLanguage, conversationId, + lowBandwidth) createPublicPost(path, nickname, domain, port, httpPrefix, "In the gardens of memory, in the palace " + "of dreams, that is where you and I shall meet", @@ -528,7 +563,9 @@ def createServerAlice(path: str, domain: str, port: int, testInReplyTo, testInReplyToAtomUri, testSubject, testSchedulePost, testEventDate, testEventTime, testLocation, - testIsArticle) + testIsArticle, systemLanguage, conversationId, + lowBandwidth) + regenerateIndexForBox(path, nickname, domain, 'outbox') global testServerAliceRunning testServerAliceRunning = True maxMentions = 10 @@ -547,8 +584,11 @@ def createServerAlice(path: str, domain: str, port: int, city = 'London, England' logLoginFailures = False userAgentsBlocked = [] + maxLikeCount = 10 print('Server running: Alice') - runDaemon(userAgentsBlocked, + runDaemon(lowBandwidth, maxLikeCount, + sharedItemsFederatedDomains, + userAgentsBlocked, logLoginFailures, city, showNodeInfoAccounts, showNodeInfoVersion, @@ -579,6 +619,8 @@ def createServerBob(path: str, domain: str, port: int, shutil.rmtree(path) os.mkdir(path) os.chdir(path) + sharedItemsFederatedDomains = [] + systemLanguage = 'en' nickname = 'bob' httpPrefix = 'http' proxyType = None @@ -588,16 +630,17 @@ def createServerBob(path: str, domain: str, port: int, domainMaxPostsPerDay = 1000 accountMaxPostsPerDay = 1000 allowDeletion = True + lowBandwidth = True privateKeyPem, publicKeyPem, person, wfEndpoint = \ createPerson(path, nickname, domain, port, httpPrefix, True, False, password) deleteAllPosts(path, nickname, domain, 'inbox') deleteAllPosts(path, nickname, domain, 'outbox') - if hasFollows: + if hasFollows and aliceAddress: followPerson(path, nickname, domain, - 'alice', aliceAddress, federationList, False) + 'alice', aliceAddress, federationList, False, False) followerOfPerson(path, nickname, domain, - 'alice', aliceAddress, federationList, False) + 'alice', aliceAddress, federationList, False, False) if hasPosts: testFollowersOnly = False testSaveToFile = True @@ -614,6 +657,7 @@ def createServerBob(path: str, domain: str, port: int, testEventTime = None testLocation = None testIsArticle = False + conversationId = None createPublicPost(path, nickname, domain, port, httpPrefix, "It's your life, live it your way.", testFollowersOnly, @@ -626,7 +670,8 @@ def createServerBob(path: str, domain: str, port: int, testInReplyTo, testInReplyToAtomUri, testSubject, testSchedulePost, testEventDate, testEventTime, testLocation, - testIsArticle) + testIsArticle, systemLanguage, conversationId, + lowBandwidth) createPublicPost(path, nickname, domain, port, httpPrefix, "One of the things I've realised is that " + "I am very simple", @@ -640,7 +685,8 @@ def createServerBob(path: str, domain: str, port: int, testInReplyTo, testInReplyToAtomUri, testSubject, testSchedulePost, testEventDate, testEventTime, testLocation, - testIsArticle) + testIsArticle, systemLanguage, conversationId, + lowBandwidth) createPublicPost(path, nickname, domain, port, httpPrefix, "Quantum physics is a bit of a passion of mine", testFollowersOnly, @@ -653,7 +699,9 @@ def createServerBob(path: str, domain: str, port: int, testInReplyTo, testInReplyToAtomUri, testSubject, testSchedulePost, testEventDate, testEventTime, testLocation, - testIsArticle) + testIsArticle, systemLanguage, conversationId, + lowBandwidth) + regenerateIndexForBox(path, nickname, domain, 'outbox') global testServerBobRunning testServerBobRunning = True maxMentions = 10 @@ -672,8 +720,11 @@ def createServerBob(path: str, domain: str, port: int, city = 'London, England' logLoginFailures = False userAgentsBlocked = [] + maxLikeCount = 10 print('Server running: Bob') - runDaemon(userAgentsBlocked, + runDaemon(lowBandwidth, maxLikeCount, + sharedItemsFederatedDomains, + userAgentsBlocked, logLoginFailures, city, showNodeInfoAccounts, showNodeInfoVersion, @@ -703,6 +754,7 @@ def createServerEve(path: str, domain: str, port: int, federationList: [], shutil.rmtree(path) os.mkdir(path) os.chdir(path) + sharedItemsFederatedDomains = [] nickname = 'eve' httpPrefix = 'http' proxyType = None @@ -732,8 +784,12 @@ def createServerEve(path: str, domain: str, port: int, federationList: [], city = 'London, England' logLoginFailures = False userAgentsBlocked = [] + maxLikeCount = 10 + lowBandwidth = True print('Server running: Eve') - runDaemon(userAgentsBlocked, + runDaemon(lowBandwidth, maxLikeCount, + sharedItemsFederatedDomains, + userAgentsBlocked, logLoginFailures, city, showNodeInfoAccounts, showNodeInfoVersion, @@ -753,6 +809,75 @@ def createServerEve(path: str, domain: str, port: int, federationList: [], sendThreads, False) +def createServerGroup(path: str, domain: str, port: int, + federationList: [], + hasFollows: bool, hasPosts: bool, + sendThreads: []): + print('Creating test server: Group on port ' + str(port)) + if os.path.isdir(path): + shutil.rmtree(path) + os.mkdir(path) + os.chdir(path) + sharedItemsFederatedDomains = [] + # systemLanguage = 'en' + nickname = 'testgroup' + httpPrefix = 'http' + proxyType = None + password = 'testgrouppass' + maxReplies = 64 + domainMaxPostsPerDay = 1000 + accountMaxPostsPerDay = 1000 + allowDeletion = True + privateKeyPem, publicKeyPem, person, wfEndpoint = \ + createGroup(path, nickname, domain, port, httpPrefix, True, + password) + deleteAllPosts(path, nickname, domain, 'inbox') + deleteAllPosts(path, nickname, domain, 'outbox') + global testServerGroupRunning + testServerGroupRunning = True + maxMentions = 10 + maxEmoji = 10 + onionDomain = None + i2pDomain = None + allowLocalNetworkAccess = True + maxNewswirePosts = 20 + dormantMonths = 3 + sendThreadsTimeoutMins = 30 + maxFollowers = 10 + verifyAllSignatures = True + brochMode = False + showNodeInfoAccounts = True + showNodeInfoVersion = True + city = 'London, England' + logLoginFailures = False + userAgentsBlocked = [] + maxLikeCount = 10 + lowBandwidth = True + print('Server running: Group') + runDaemon(lowBandwidth, maxLikeCount, + sharedItemsFederatedDomains, + userAgentsBlocked, + logLoginFailures, city, + showNodeInfoAccounts, + showNodeInfoVersion, + brochMode, + verifyAllSignatures, + sendThreadsTimeoutMins, + dormantMonths, maxNewswirePosts, + allowLocalNetworkAccess, + 2048, False, True, False, False, True, maxFollowers, + 0, 100, 1024, 5, False, + 0, False, 1, False, False, False, + 5, True, True, 'en', __version__, + "instanceId", False, path, domain, + onionDomain, i2pDomain, None, port, port, + httpPrefix, federationList, maxMentions, maxEmoji, False, + proxyType, maxReplies, + domainMaxPostsPerDay, accountMaxPostsPerDay, + allowDeletion, True, True, False, sendThreads, + False) + + def testPostMessageBetweenServers(): print('Testing sending message from one server to the inbox of another') @@ -761,6 +886,7 @@ def testPostMessageBetweenServers(): testServerAliceRunning = False testServerBobRunning = False + systemLanguage = 'en' httpPrefix = 'http' proxyType = None @@ -836,6 +962,8 @@ def testPostMessageBetweenServers(): ccUrl = None alicePersonCache = {} aliceCachedWebfingers = {} + aliceSharedItemsFederatedDomains = [] + aliceSharedItemFederationTokens = {} attachedImageFilename = baseDir + '/img/logo.png' testImageWidth, testImageHeight = \ getImageDimensions(attachedImageFilename) @@ -849,7 +977,7 @@ def testPostMessageBetweenServers(): outboxPath = aliceDir + '/accounts/alice@' + aliceDomain + '/outbox' assert len([name for name in os.listdir(outboxPath) if os.path.isfile(os.path.join(outboxPath, name))]) == 0 - + lowBandwidth = False sendResult = \ sendPost(__version__, sessionAlice, aliceDir, 'alice', aliceDomain, alicePort, @@ -861,8 +989,10 @@ def testPostMessageBetweenServers(): attachedImageFilename, mediaType, attachedImageDescription, city, federationList, aliceSendThreads, alicePostLog, aliceCachedWebfingers, - alicePersonCache, isArticle, inReplyTo, - inReplyToAtomUri, subject) + alicePersonCache, isArticle, systemLanguage, + aliceSharedItemsFederatedDomains, + aliceSharedItemFederationTokens, lowBandwidth, + inReplyTo, inReplyToAtomUri, subject) print('sendResult: ' + str(sendResult)) queuePath = bobDir + '/accounts/bob@' + bobDomain + '/queue' @@ -923,6 +1053,8 @@ def testPostMessageBetweenServers(): assert receivedJson assert 'Why is a mouse when it spins?' in \ receivedJson['object']['content'] + assert 'Why is a mouse when it spins?' in \ + receivedJson['object']['contentMap'][systemLanguage] assert 'यह एक परीक्षण है' in receivedJson['object']['content'] print('Check that message received from Alice contains an attachment') assert receivedJson['object']['attachment'] @@ -944,10 +1076,10 @@ def testPostMessageBetweenServers(): aliceDomainStr = aliceDomain + ':' + str(alicePort) followerOfPerson(bobDir, 'bob', bobDomain, 'alice', - aliceDomainStr, federationList, False) + aliceDomainStr, federationList, False, False) bobDomainStr = bobDomain + ':' + str(bobPort) followPerson(aliceDir, 'alice', aliceDomain, 'bob', - bobDomainStr, federationList, False) + bobDomainStr, federationList, False, False) sessionBob = createSession(proxyType) bobPostLog = [] @@ -1052,6 +1184,7 @@ def testFollowBetweenServers(): testServerAliceRunning = False testServerBobRunning = False + systemLanguage = 'en' httpPrefix = 'http' proxyType = None federationList = [] @@ -1168,15 +1301,20 @@ def testFollowBetweenServers(): assert 'bob@' + bobDomain in open(aliceDir + '/accounts/alice@' + aliceDomain + '/followingCalendar.txt').read() + assert not isGroupActor(aliceDir, bobActor, alicePersonCache) + assert not isGroupAccount(aliceDir, 'alice', aliceDomain) print('\n\n*********************************************************') print('Alice sends a message to Bob') alicePostLog = [] alicePersonCache = {} aliceCachedWebfingers = {} + aliceSharedItemsFederatedDomains = [] + aliceSharedItemFederationTokens = {} alicePostLog = [] isArticle = False city = 'London, England' + lowBandwidth = False sendResult = \ sendPost(__version__, sessionAlice, aliceDir, 'alice', aliceDomain, alicePort, @@ -1185,8 +1323,10 @@ def testFollowBetweenServers(): clientToServer, True, None, None, None, city, federationList, aliceSendThreads, alicePostLog, aliceCachedWebfingers, - alicePersonCache, isArticle, inReplyTo, - inReplyToAtomUri, subject) + alicePersonCache, isArticle, systemLanguage, + aliceSharedItemsFederatedDomains, + aliceSharedItemFederationTokens, lowBandwidth, + inReplyTo, inReplyToAtomUri, subject) print('sendResult: ' + str(sendResult)) queuePath = bobDir + '/accounts/bob@' + bobDomain + '/queue' @@ -1222,6 +1362,745 @@ def testFollowBetweenServers(): shutil.rmtree(baseDir + '/.tests') +def testSharedItemsFederation(): + print('Testing federation of shared items between Alice and Bob') + + global testServerAliceRunning + global testServerBobRunning + testServerAliceRunning = False + testServerBobRunning = False + + systemLanguage = 'en' + httpPrefix = 'http' + proxyType = None + federationList = [] + + baseDir = os.getcwd() + if os.path.isdir(baseDir + '/.tests'): + shutil.rmtree(baseDir + '/.tests') + os.mkdir(baseDir + '/.tests') + + # create the servers + aliceDir = baseDir + '/.tests/alice' + aliceDomain = '127.0.0.74' + alicePort = 61917 + aliceSendThreads = [] + aliceAddress = aliceDomain + ':' + str(alicePort) + + bobDir = baseDir + '/.tests/bob' + bobDomain = '127.0.0.81' + bobPort = 61983 + bobSendThreads = [] + bobAddress = bobDomain + ':' + str(bobPort) + bobPassword = 'bobpass' + bobCachedWebfingers = {} + bobPersonCache = {} + + global thrAlice + if thrAlice: + while thrAlice.is_alive(): + thrAlice.stop() + time.sleep(1) + thrAlice.kill() + + thrAlice = \ + threadWithTrace(target=createServerAlice, + args=(aliceDir, aliceDomain, alicePort, bobAddress, + federationList, False, False, + aliceSendThreads), + daemon=True) + + global thrBob + if thrBob: + while thrBob.is_alive(): + thrBob.stop() + time.sleep(1) + thrBob.kill() + + thrBob = \ + threadWithTrace(target=createServerBob, + args=(bobDir, bobDomain, bobPort, aliceAddress, + federationList, False, False, + bobSendThreads), + daemon=True) + + thrAlice.start() + thrBob.start() + assert thrAlice.is_alive() is True + assert thrBob.is_alive() is True + + # wait for all servers to be running + ctr = 0 + while not (testServerAliceRunning and testServerBobRunning): + time.sleep(1) + ctr += 1 + if ctr > 60: + break + print('Alice online: ' + str(testServerAliceRunning)) + print('Bob online: ' + str(testServerBobRunning)) + assert ctr <= 60 + time.sleep(1) + + # In the beginning all was calm and there were no follows + + print('\n\n*********************************************************') + print("Alice and Bob agree to share items catalogs") + assert os.path.isdir(aliceDir) + assert os.path.isdir(bobDir) + setConfigParam(aliceDir, 'sharedItemsFederatedDomains', bobAddress) + setConfigParam(bobDir, 'sharedItemsFederatedDomains', aliceAddress) + + print('*********************************************************') + print('Alice sends a follow request to Bob') + os.chdir(aliceDir) + sessionAlice = createSession(proxyType) + inReplyTo = None + inReplyToAtomUri = None + subject = None + alicePostLog = [] + followersOnly = False + saveToFile = True + clientToServer = False + ccUrl = None + alicePersonCache = {} + aliceCachedWebfingers = {} + alicePostLog = [] + bobActor = httpPrefix + '://' + bobAddress + '/users/bob' + sendResult = \ + sendFollowRequest(sessionAlice, aliceDir, + 'alice', aliceDomain, alicePort, httpPrefix, + 'bob', bobDomain, bobActor, + bobPort, httpPrefix, + clientToServer, federationList, + aliceSendThreads, alicePostLog, + aliceCachedWebfingers, alicePersonCache, + True, __version__) + print('sendResult: ' + str(sendResult)) + + for t in range(16): + if os.path.isfile(bobDir + '/accounts/bob@' + + bobDomain + '/followers.txt'): + if os.path.isfile(aliceDir + '/accounts/alice@' + + aliceDomain + '/following.txt'): + if os.path.isfile(aliceDir + '/accounts/alice@' + + aliceDomain + '/followingCalendar.txt'): + break + time.sleep(1) + + assert validInbox(bobDir, 'bob', bobDomain) + assert validInboxFilenames(bobDir, 'bob', bobDomain, + aliceDomain, alicePort) + assert 'alice@' + aliceDomain in open(bobDir + '/accounts/bob@' + + bobDomain + '/followers.txt').read() + assert 'bob@' + bobDomain in open(aliceDir + '/accounts/alice@' + + aliceDomain + '/following.txt').read() + assert 'bob@' + bobDomain in open(aliceDir + '/accounts/alice@' + + aliceDomain + + '/followingCalendar.txt').read() + assert not isGroupActor(aliceDir, bobActor, alicePersonCache) + assert not isGroupAccount(bobDir, 'bob', bobDomain) + + print('\n\n*********************************************************') + print('Bob publishes some shared items') + if os.path.isdir(bobDir + '/ontology'): + shutil.rmtree(bobDir + '/ontology') + os.mkdir(bobDir + '/ontology') + copyfile(baseDir + '/img/logo.png', bobDir + '/logo.png') + copyfile(baseDir + '/ontology/foodTypes.json', + bobDir + '/ontology/foodTypes.json') + copyfile(baseDir + '/ontology/toolTypes.json', + bobDir + '/ontology/toolTypes.json') + copyfile(baseDir + '/ontology/clothesTypes.json', + bobDir + '/ontology/clothesTypes.json') + assert os.path.isfile(bobDir + '/logo.png') + assert os.path.isfile(bobDir + '/ontology/foodTypes.json') + assert os.path.isfile(bobDir + '/ontology/toolTypes.json') + assert os.path.isfile(bobDir + '/ontology/clothesTypes.json') + sessionBob = createSession(proxyType) + sharedItemName = 'cheddar' + sharedItemDescription = 'Some cheese' + sharedItemImageFilename = 'logo.png' + sharedItemQty = 1 + sharedItemType = 'Cheese' + sharedItemCategory = 'Food' + sharedItemLocation = "Bob's location" + sharedItemDuration = "10 days" + sharedItemPrice = "1.30" + sharedItemCurrency = "EUR" + shareJson = \ + sendShareViaServer(bobDir, sessionBob, + 'bob', bobPassword, + bobDomain, bobPort, + httpPrefix, sharedItemName, + sharedItemDescription, sharedItemImageFilename, + sharedItemQty, sharedItemType, sharedItemCategory, + sharedItemLocation, sharedItemDuration, + bobCachedWebfingers, bobPersonCache, + True, __version__, + sharedItemPrice, sharedItemCurrency) + assert shareJson + assert isinstance(shareJson, dict) + sharedItemName = 'Epicyon T-shirt' + sharedItemDescription = 'A fashionable item' + sharedItemImageFilename = 'logo.png' + sharedItemQty = 1 + sharedItemType = 'T-Shirt' + sharedItemCategory = 'Clothes' + sharedItemLocation = "Bob's location" + sharedItemDuration = "5 days" + sharedItemPrice = "0" + sharedItemCurrency = "EUR" + shareJson = \ + sendShareViaServer(bobDir, sessionBob, + 'bob', bobPassword, + bobDomain, bobPort, + httpPrefix, sharedItemName, + sharedItemDescription, sharedItemImageFilename, + sharedItemQty, sharedItemType, sharedItemCategory, + sharedItemLocation, sharedItemDuration, + bobCachedWebfingers, bobPersonCache, + True, __version__, + sharedItemPrice, sharedItemCurrency) + assert shareJson + assert isinstance(shareJson, dict) + sharedItemName = 'Soldering iron' + sharedItemDescription = 'A soldering iron' + sharedItemImageFilename = 'logo.png' + sharedItemQty = 1 + sharedItemType = 'Soldering iron' + sharedItemCategory = 'Tools' + sharedItemLocation = "Bob's location" + sharedItemDuration = "9 days" + sharedItemPrice = "10.00" + sharedItemCurrency = "EUR" + shareJson = \ + sendShareViaServer(bobDir, sessionBob, + 'bob', bobPassword, + bobDomain, bobPort, + httpPrefix, sharedItemName, + sharedItemDescription, sharedItemImageFilename, + sharedItemQty, sharedItemType, sharedItemCategory, + sharedItemLocation, sharedItemDuration, + bobCachedWebfingers, bobPersonCache, + True, __version__, + sharedItemPrice, sharedItemCurrency) + assert shareJson + assert isinstance(shareJson, dict) + + time.sleep(2) + print('\n\n*********************************************************') + print('Bob has a shares.json file containing the uploaded items') + + sharesFilename = bobDir + '/accounts/bob@' + bobDomain + '/shares.json' + assert os.path.isfile(sharesFilename) + sharesJson = loadJson(sharesFilename) + assert sharesJson + pprint(sharesJson) + assert len(sharesJson.items()) == 3 + for itemID, item in sharesJson.items(): + if not item.get('dfcId'): + pprint(item) + print(itemID + ' does not have dfcId field') + assert item.get('dfcId') + + print('\n\n*********************************************************') + print('Bob can read the shared items catalog on his own instance') + catalogJson = \ + getSharedItemsCatalogViaServer(bobDir, sessionBob, 'bob', bobPassword, + bobDomain, bobPort, httpPrefix, True) + assert catalogJson + pprint(catalogJson) + assert 'DFC:supplies' in catalogJson + assert len(catalogJson.get('DFC:supplies')) == 3 + + print('\n\n*********************************************************') + print('Alice sends a message to Bob') + aliceTokensFilename = \ + aliceDir + '/accounts/sharedItemsFederationTokens.json' + assert os.path.isfile(aliceTokensFilename) + aliceSharedItemFederationTokens = loadJson(aliceTokensFilename) + assert aliceSharedItemFederationTokens + print('Alice shared item federation tokens:') + pprint(aliceSharedItemFederationTokens) + assert len(aliceSharedItemFederationTokens.items()) > 0 + for hostStr, token in aliceSharedItemFederationTokens.items(): + assert ':' in hostStr + alicePostLog = [] + alicePersonCache = {} + aliceCachedWebfingers = {} + aliceSharedItemsFederatedDomains = [bobAddress] + alicePostLog = [] + isArticle = False + city = 'London, England' + lowBandwidth = False + sendResult = \ + sendPost(__version__, + sessionAlice, aliceDir, 'alice', aliceDomain, alicePort, + 'bob', bobDomain, bobPort, ccUrl, + httpPrefix, 'Alice message', followersOnly, saveToFile, + clientToServer, True, + None, None, None, city, federationList, + aliceSendThreads, alicePostLog, aliceCachedWebfingers, + alicePersonCache, isArticle, systemLanguage, + aliceSharedItemsFederatedDomains, + aliceSharedItemFederationTokens, lowBandwidth, True, + inReplyTo, inReplyToAtomUri, subject) + print('sendResult: ' + str(sendResult)) + + queuePath = bobDir + '/accounts/bob@' + bobDomain + '/queue' + inboxPath = bobDir + '/accounts/bob@' + bobDomain + '/inbox' + aliceMessageArrived = False + for i in range(20): + time.sleep(1) + if os.path.isdir(inboxPath): + if len([name for name in os.listdir(inboxPath) + if os.path.isfile(os.path.join(inboxPath, name))]) > 0: + aliceMessageArrived = True + print('Alice message sent to Bob!') + break + + assert aliceMessageArrived is True + print('Message from Alice to Bob succeeded') + + print('\n\n*********************************************************') + print('Check that Alice received the shared items authorization') + print('token from Bob') + aliceTokensFilename = \ + aliceDir + '/accounts/sharedItemsFederationTokens.json' + bobTokensFilename = \ + bobDir + '/accounts/sharedItemsFederationTokens.json' + assert os.path.isfile(aliceTokensFilename) + assert os.path.isfile(bobTokensFilename) + aliceTokens = loadJson(aliceTokensFilename) + assert aliceTokens + for hostStr, token in aliceTokens.items(): + assert ':' in hostStr + assert aliceTokens.get(aliceAddress) + print('Alice tokens') + pprint(aliceTokens) + bobTokens = loadJson(bobTokensFilename) + assert bobTokens + for hostStr, token in bobTokens.items(): + assert ':' in hostStr + assert bobTokens.get(bobAddress) + print("Check that Bob now has Alice's token") + assert bobTokens.get(aliceAddress) + print('Bob tokens') + pprint(bobTokens) + + print('\n\n*********************************************************') + print('Alice can read the federated shared items catalog of Bob') + headers = { + 'Origin': aliceAddress, + 'Authorization': bobTokens[bobAddress], + 'host': bobAddress, + 'Accept': 'application/json' + } + url = httpPrefix + '://' + bobAddress + '/catalog' + catalogJson = getJson(sessionAlice, url, headers, None, True) + assert catalogJson + pprint(catalogJson) + assert 'DFC:supplies' in catalogJson + assert len(catalogJson.get('DFC:supplies')) == 3 + + # stop the servers + thrAlice.kill() + thrAlice.join() + assert thrAlice.is_alive() is False + + thrBob.kill() + thrBob.join() + assert thrBob.is_alive() is False + + # queue item removed + time.sleep(4) + assert len([name for name in os.listdir(queuePath) + if os.path.isfile(os.path.join(queuePath, name))]) == 0 + + os.chdir(baseDir) + shutil.rmtree(baseDir + '/.tests') + print('Testing federation of shared items between ' + + 'Alice and Bob is complete') + + +def testGroupFollow(): + print('Testing following of a group') + + global testServerAliceRunning + global testServerBobRunning + global testServerGroupRunning + systemLanguage = 'en' + testServerAliceRunning = False + testServerBobRunning = False + testServerGroupRunning = False + + # systemLanguage = 'en' + httpPrefix = 'http' + proxyType = None + federationList = [] + + baseDir = os.getcwd() + if os.path.isdir(baseDir + '/.tests'): + shutil.rmtree(baseDir + '/.tests') + os.mkdir(baseDir + '/.tests') + + # create the servers + aliceDir = baseDir + '/.tests/alice' + aliceDomain = '127.0.0.57' + alicePort = 61927 + aliceSendThreads = [] + aliceAddress = aliceDomain + ':' + str(alicePort) + + bobDir = baseDir + '/.tests/bob' + bobDomain = '127.0.0.59' + bobPort = 61814 + bobSendThreads = [] + # bobAddress = bobDomain + ':' + str(bobPort) + + testgroupDir = baseDir + '/.tests/testgroup' + testgroupDomain = '127.0.0.63' + testgroupPort = 61925 + testgroupSendThreads = [] + testgroupAddress = testgroupDomain + ':' + str(testgroupPort) + + global thrAlice + if thrAlice: + while thrAlice.is_alive(): + thrAlice.stop() + time.sleep(1) + thrAlice.kill() + + thrAlice = \ + threadWithTrace(target=createServerAlice, + args=(aliceDir, aliceDomain, alicePort, + testgroupAddress, + federationList, False, True, + aliceSendThreads), + daemon=True) + + global thrBob + if thrBob: + while thrBob.is_alive(): + thrBob.stop() + time.sleep(1) + thrBob.kill() + + thrBob = \ + threadWithTrace(target=createServerBob, + args=(bobDir, bobDomain, bobPort, None, + federationList, False, False, + bobSendThreads), + daemon=True) + + global thrGroup + if thrGroup: + while thrGroup.is_alive(): + thrGroup.stop() + time.sleep(1) + thrGroup.kill() + + thrGroup = \ + threadWithTrace(target=createServerGroup, + args=(testgroupDir, testgroupDomain, testgroupPort, + federationList, False, False, + testgroupSendThreads), + daemon=True) + + thrAlice.start() + thrBob.start() + thrGroup.start() + assert thrAlice.is_alive() is True + assert thrBob.is_alive() is True + assert thrGroup.is_alive() is True + + # wait for all servers to be running + ctr = 0 + while not (testServerAliceRunning and + testServerBobRunning and + testServerGroupRunning): + time.sleep(1) + ctr += 1 + if ctr > 60: + break + print('Alice online: ' + str(testServerAliceRunning)) + print('Bob online: ' + str(testServerBobRunning)) + print('Test Group online: ' + str(testServerGroupRunning)) + assert ctr <= 60 + time.sleep(1) + + print('*********************************************************') + print('Alice has some outbox posts') + aliceOutbox = 'http://' + aliceAddress + '/users/alice/outbox' + session = createSession(None) + profileStr = 'https://www.w3.org/ns/activitystreams' + asHeader = { + 'Accept': 'application/ld+json; profile="' + profileStr + '"' + } + outboxJson = getJson(session, aliceOutbox, asHeader, None, + True, __version__, 'http', None) + assert outboxJson + pprint(outboxJson) + assert outboxJson['type'] == 'OrderedCollection' + assert 'first' in outboxJson + firstPage = outboxJson['first'] + assert 'totalItems' in outboxJson + print('Alice outbox totalItems: ' + str(outboxJson['totalItems'])) + assert outboxJson['totalItems'] == 3 + + outboxJson = getJson(session, firstPage, asHeader, None, + True, __version__, 'http', None) + assert outboxJson + pprint(outboxJson) + assert 'orderedItems' in outboxJson + assert outboxJson['type'] == 'OrderedCollectionPage' + print('Alice outbox orderedItems: ' + + str(len(outboxJson['orderedItems']))) + assert len(outboxJson['orderedItems']) == 3 + + queuePath = \ + testgroupDir + '/accounts/testgroup@' + testgroupDomain + '/queue' + + # In the beginning the test group had no followers + + print('*********************************************************') + print('Alice sends a follow request to the test group') + os.chdir(aliceDir) + sessionAlice = createSession(proxyType) + inReplyTo = None + inReplyToAtomUri = None + subject = None + alicePostLog = [] + followersOnly = False + saveToFile = True + clientToServer = False + ccUrl = None + alicePersonCache = {} + aliceCachedWebfingers = {} + alicePostLog = [] + # aliceActor = httpPrefix + '://' + aliceAddress + '/users/alice' + testgroupActor = httpPrefix + '://' + testgroupAddress + '/users/testgroup' + sendResult = \ + sendFollowRequest(sessionAlice, aliceDir, + 'alice', aliceDomain, alicePort, httpPrefix, + 'testgroup', testgroupDomain, testgroupActor, + testgroupPort, httpPrefix, + clientToServer, federationList, + aliceSendThreads, alicePostLog, + aliceCachedWebfingers, alicePersonCache, + True, __version__) + print('sendResult: ' + str(sendResult)) + + aliceFollowingFilename = \ + aliceDir + '/accounts/alice@' + aliceDomain + '/following.txt' + aliceFollowingCalendarFilename = \ + aliceDir + '/accounts/alice@' + aliceDomain + \ + '/followingCalendar.txt' + testgroupFollowersFilename = \ + testgroupDir + '/accounts/testgroup@' + testgroupDomain + \ + '/followers.txt' + + for t in range(16): + if os.path.isfile(testgroupFollowersFilename): + if os.path.isfile(aliceFollowingFilename): + if os.path.isfile(aliceFollowingCalendarFilename): + break + time.sleep(1) + + assert validInbox(testgroupDir, 'testgroup', testgroupDomain) + assert validInboxFilenames(testgroupDir, 'testgroup', testgroupDomain, + aliceDomain, alicePort) + assert 'alice@' + aliceDomain in open(testgroupFollowersFilename).read() + assert '!alice@' + aliceDomain not in \ + open(testgroupFollowersFilename).read() + + testgroupWebfingerFilename = \ + testgroupDir + '/wfendpoints/testgroup@' + \ + testgroupDomain + ':' + str(testgroupPort) + '.json' + assert os.path.isfile(testgroupWebfingerFilename) + assert 'group:testgroup@' in open(testgroupWebfingerFilename).read() + print('group: exists within the webfinger endpoint for testgroup') + + testgroupHandle = 'testgroup@' + testgroupDomain + followingStr = '' + with open(aliceFollowingFilename, 'r') as fp: + followingStr = fp.read() + print('Alice following.txt:\n\n' + followingStr) + if '!testgroup' not in followingStr: + print('Alice following.txt does not contain !testgroup@' + + testgroupDomain + ':' + str(testgroupPort)) + assert isGroupActor(aliceDir, testgroupActor, alicePersonCache) + assert not isGroupAccount(aliceDir, 'alice', aliceDomain) + assert isGroupAccount(testgroupDir, 'testgroup', testgroupDomain) + assert '!testgroup' in followingStr + assert testgroupHandle in open(aliceFollowingFilename).read() + assert testgroupHandle in open(aliceFollowingCalendarFilename).read() + print('\n\n*********************************************************') + print('Alice follows the test group') + + print('*********************************************************') + print('Bob sends a follow request to the test group') + os.chdir(bobDir) + sessionBob = createSession(proxyType) + inReplyTo = None + inReplyToAtomUri = None + subject = None + bobPostLog = [] + followersOnly = False + saveToFile = True + clientToServer = False + ccUrl = None + bobPersonCache = {} + bobCachedWebfingers = {} + bobPostLog = [] + # bobActor = httpPrefix + '://' + bobAddress + '/users/bob' + testgroupActor = httpPrefix + '://' + testgroupAddress + '/users/testgroup' + sendResult = \ + sendFollowRequest(sessionBob, bobDir, + 'bob', bobDomain, bobPort, httpPrefix, + 'testgroup', testgroupDomain, testgroupActor, + testgroupPort, httpPrefix, + clientToServer, federationList, + bobSendThreads, bobPostLog, + bobCachedWebfingers, bobPersonCache, + True, __version__) + print('sendResult: ' + str(sendResult)) + + bobFollowingFilename = \ + bobDir + '/accounts/bob@' + bobDomain + '/following.txt' + bobFollowingCalendarFilename = \ + bobDir + '/accounts/bob@' + bobDomain + \ + '/followingCalendar.txt' + testgroupFollowersFilename = \ + testgroupDir + '/accounts/testgroup@' + testgroupDomain + \ + '/followers.txt' + + for t in range(16): + if os.path.isfile(testgroupFollowersFilename): + if os.path.isfile(bobFollowingFilename): + if os.path.isfile(bobFollowingCalendarFilename): + break + time.sleep(1) + + assert validInbox(testgroupDir, 'testgroup', testgroupDomain) + assert validInboxFilenames(testgroupDir, 'testgroup', testgroupDomain, + bobDomain, bobPort) + assert 'bob@' + bobDomain in open(testgroupFollowersFilename).read() + assert '!bob@' + bobDomain not in open(testgroupFollowersFilename).read() + + testgroupWebfingerFilename = \ + testgroupDir + '/wfendpoints/testgroup@' + \ + testgroupDomain + ':' + str(testgroupPort) + '.json' + assert os.path.isfile(testgroupWebfingerFilename) + assert 'group:testgroup@' in open(testgroupWebfingerFilename).read() + print('group: exists within the webfinger endpoint for testgroup') + + testgroupHandle = 'testgroup@' + testgroupDomain + followingStr = '' + with open(bobFollowingFilename, 'r') as fp: + followingStr = fp.read() + print('Bob following.txt:\n\n' + followingStr) + if '!testgroup' not in followingStr: + print('Bob following.txt does not contain !testgroup@' + + testgroupDomain + ':' + str(testgroupPort)) + assert isGroupActor(bobDir, testgroupActor, bobPersonCache) + assert '!testgroup' in followingStr + assert testgroupHandle in open(bobFollowingFilename).read() + assert testgroupHandle in open(bobFollowingCalendarFilename).read() + print('Bob follows the test group') + + print('\n\n*********************************************************') + print('Alice posts to the test group') + inboxPathBob = \ + bobDir + '/accounts/bob@' + bobDomain + '/inbox' + startPostsBob = \ + len([name for name in os.listdir(inboxPathBob) + if os.path.isfile(os.path.join(inboxPathBob, name))]) + assert startPostsBob == 0 + alicePostLog = [] + alicePersonCache = {} + aliceCachedWebfingers = {} + aliceSharedItemsFederatedDomains = [] + aliceSharedItemFederationTokens = {} + alicePostLog = [] + isArticle = False + city = 'London, England' + lowBandwidth = False + sendResult = \ + sendPost(__version__, + sessionAlice, aliceDir, 'alice', aliceDomain, alicePort, + 'testgroup', testgroupDomain, testgroupPort, ccUrl, + httpPrefix, "Alice group message", followersOnly, + saveToFile, clientToServer, True, + None, None, None, city, federationList, + aliceSendThreads, alicePostLog, aliceCachedWebfingers, + alicePersonCache, isArticle, systemLanguage, + aliceSharedItemsFederatedDomains, + aliceSharedItemFederationTokens, lowBandwidth, + inReplyTo, inReplyToAtomUri, subject) + print('sendResult: ' + str(sendResult)) + + queuePath = \ + testgroupDir + '/accounts/testgroup@' + testgroupDomain + '/queue' + inboxPath = \ + testgroupDir + '/accounts/testgroup@' + testgroupDomain + '/inbox' + aliceMessageArrived = False + startPosts = len([name for name in os.listdir(inboxPath) + if os.path.isfile(os.path.join(inboxPath, name))]) + for i in range(20): + time.sleep(1) + if os.path.isdir(inboxPath): + currPosts = \ + len([name for name in os.listdir(inboxPath) + if os.path.isfile(os.path.join(inboxPath, name))]) + if currPosts > startPosts: + aliceMessageArrived = True + print('Alice post sent to test group!') + break + + assert aliceMessageArrived is True + print('\n\n*********************************************************') + print('Post from Alice to test group succeeded') + + print('\n\n*********************************************************') + print('Check that post was relayed from test group to bob') + + bobMessageArrived = False + for i in range(20): + time.sleep(1) + if os.path.isdir(inboxPathBob): + currPostsBob = \ + len([name for name in os.listdir(inboxPathBob) + if os.path.isfile(os.path.join(inboxPathBob, name))]) + if currPostsBob > startPostsBob: + bobMessageArrived = True + print('Bob received relayed group post!') + break + + assert bobMessageArrived is True + + # stop the servers + thrAlice.kill() + thrAlice.join() + assert thrAlice.is_alive() is False + + thrBob.kill() + thrBob.join() + assert thrBob.is_alive() is False + + thrGroup.kill() + thrGroup.join() + assert thrGroup.is_alive() is False + + # queue item removed + time.sleep(4) + assert len([name for name in os.listdir(queuePath) + if os.path.isfile(os.path.join(queuePath, name))]) == 0 + + os.chdir(baseDir) + shutil.rmtree(baseDir + '/.tests') + print('Testing following of a group is complete') + + def _testFollowersOfPerson(): print('testFollowersOfPerson') currDir = os.getcwd() @@ -1249,18 +2128,18 @@ def _testFollowersOfPerson(): clearFollows(baseDir, nickname, domain) followPerson(baseDir, nickname, domain, 'maxboardroom', domain, - federationList, False) + federationList, False, False) followPerson(baseDir, 'drokk', domain, 'ultrapancake', domain, - federationList, False) + federationList, False, False) # deliberate duplication followPerson(baseDir, 'drokk', domain, 'ultrapancake', domain, - federationList, False) + federationList, False, False) followPerson(baseDir, 'sausagedog', domain, 'ultrapancake', domain, - federationList, False) + federationList, False, False) followPerson(baseDir, nickname, domain, 'ultrapancake', domain, - federationList, False) + federationList, False, False) followPerson(baseDir, nickname, domain, 'someother', 'randodomain.net', - federationList, False) + federationList, False, False) followList = getFollowersOfPerson(baseDir, 'ultrapancake', domain) assert len(followList) == 3 @@ -1298,32 +2177,33 @@ def _testNoOfFollowersOnDomain(): httpPrefix, True, False, password) followPerson(baseDir, 'drokk', otherdomain, nickname, domain, - federationList, False) + federationList, False, False) followPerson(baseDir, 'sausagedog', otherdomain, nickname, domain, - federationList, False) + federationList, False, False) followPerson(baseDir, 'maxboardroom', otherdomain, nickname, domain, - federationList, False) + federationList, False, False) followerOfPerson(baseDir, nickname, domain, 'cucumber', 'sandwiches.party', - federationList, False) + federationList, False, False) followerOfPerson(baseDir, nickname, domain, 'captainsensible', 'damned.zone', - federationList, False) + federationList, False, False) followerOfPerson(baseDir, nickname, domain, 'pilchard', 'zombies.attack', - federationList, False) + federationList, False, False) followerOfPerson(baseDir, nickname, domain, 'drokk', otherdomain, - federationList, False) + federationList, False, False) followerOfPerson(baseDir, nickname, domain, 'sausagedog', otherdomain, - federationList, False) + federationList, False, False) followerOfPerson(baseDir, nickname, domain, 'maxboardroom', otherdomain, - federationList, False) + federationList, False, False) followersOnOtherDomain = \ noOfFollowersOnDomain(baseDir, nickname + '@' + domain, otherdomain) assert followersOnOtherDomain == 3 - unfollowerOfAccount(baseDir, nickname, domain, 'sausagedog', otherdomain) + unfollowerOfAccount(baseDir, nickname, domain, 'sausagedog', otherdomain, + False, False) followersOnOtherDomain = \ noOfFollowersOnDomain(baseDir, nickname + '@' + domain, otherdomain) assert followersOnOtherDomain == 2 @@ -1352,17 +2232,17 @@ def _testGroupFollowers(): clearFollowers(baseDir, nickname, domain) followerOfPerson(baseDir, nickname, domain, 'badger', 'wild.domain', - federationList, False) + federationList, False, False) followerOfPerson(baseDir, nickname, domain, 'squirrel', 'wild.domain', - federationList, False) + federationList, False, False) followerOfPerson(baseDir, nickname, domain, 'rodent', 'wild.domain', - federationList, False) + federationList, False, False) followerOfPerson(baseDir, nickname, domain, 'utterly', 'clutterly.domain', - federationList, False) + federationList, False, False) followerOfPerson(baseDir, nickname, domain, 'zonked', 'zzz.domain', - federationList, False) + federationList, False, False) followerOfPerson(baseDir, nickname, domain, 'nap', 'zzz.domain', - federationList, False) + federationList, False, False) grouped = groupFollowersByDomain(baseDir, nickname, domain) assert len(grouped.items()) == 3 @@ -1396,15 +2276,15 @@ def _testFollows(): clearFollows(baseDir, nickname, domain) followPerson(baseDir, nickname, domain, 'badger', 'wild.com', - federationList, False) + federationList, False, False) followPerson(baseDir, nickname, domain, 'squirrel', 'secret.com', - federationList, False) + federationList, False, False) followPerson(baseDir, nickname, domain, 'rodent', 'drainpipe.com', - federationList, False) + federationList, False, False) followPerson(baseDir, nickname, domain, 'batman', 'mesh.com', - federationList, False) + federationList, False, False) followPerson(baseDir, nickname, domain, 'giraffe', 'trees.com', - federationList, False) + federationList, False, False) accountDir = acctDir(baseDir, nickname, domain) f = open(accountDir + '/following.txt', 'r') @@ -1419,7 +2299,8 @@ def _testFollows(): assert(False) assert(domainFound) - unfollowAccount(baseDir, nickname, domain, 'batman', 'mesh.com') + unfollowAccount(baseDir, nickname, domain, 'batman', 'mesh.com', + True, False) domainFound = False for followingDomain in f: @@ -1431,15 +2312,15 @@ def _testFollows(): clearFollowers(baseDir, nickname, domain) followerOfPerson(baseDir, nickname, domain, 'badger', 'wild.com', - federationList, False) + federationList, False, False) followerOfPerson(baseDir, nickname, domain, 'squirrel', 'secret.com', - federationList, False) + federationList, False, False) followerOfPerson(baseDir, nickname, domain, 'rodent', 'drainpipe.com', - federationList, False) + federationList, False, False) followerOfPerson(baseDir, nickname, domain, 'batman', 'mesh.com', - federationList, False) + federationList, False, False) followerOfPerson(baseDir, nickname, domain, 'giraffe', 'trees.com', - federationList, False) + federationList, False, False) accountDir = acctDir(baseDir, nickname, domain) f = open(accountDir + '/followers.txt', 'r') @@ -1456,6 +2337,7 @@ def _testFollows(): def _testCreatePerson(): print('testCreatePerson') + systemLanguage = 'en' currDir = os.getcwd() nickname = 'test382' domain = 'badgerdomain.com' @@ -1493,6 +2375,8 @@ def _testCreatePerson(): commentsEnabled = True attachImageFilename = None mediaType = None + conversationId = None + lowBandwidth = True createPublicPost(baseDir, nickname, domain, port, httpPrefix, content, followersOnly, saveToFile, clientToServer, commentsEnabled, attachImageFilename, mediaType, @@ -1500,7 +2384,8 @@ def _testCreatePerson(): testInReplyTo, testInReplyToAtomUri, testSubject, testSchedulePost, testEventDate, testEventTime, testLocation, - testIsArticle) + testIsArticle, systemLanguage, conversationId, + lowBandwidth) os.chdir(currDir) shutil.rmtree(baseDir) @@ -1554,9 +2439,11 @@ def testClientToServer(): testServerAliceRunning = False testServerBobRunning = False + systemLanguage = 'en' httpPrefix = 'http' proxyType = None federationList = [] + lowBandwidth = False baseDir = os.getcwd() if os.path.isdir(baseDir + '/.tests'): @@ -1634,6 +2521,7 @@ def testClientToServer(): cachedWebfingers = {} personCache = {} password = 'alicepass' + conversationId = None outboxPath = aliceDir + '/accounts/alice@' + aliceDomain + '/outbox' inboxPath = bobDir + '/accounts/bob@' + bobDomain + '/inbox' assert len([name for name in os.listdir(outboxPath) @@ -1650,7 +2538,9 @@ def testClientToServer(): attachedImageFilename, mediaType, attachedImageDescription, city, cachedWebfingers, personCache, isArticle, - True, None, None, None) + systemLanguage, lowBandwidth, + True, None, None, + conversationId, None) print('sendResult: ' + str(sendResult)) for i in range(30): @@ -2513,8 +3403,9 @@ def _testValidContentWarning(): def _testTranslations(): print('testTranslations') - languagesStr = ('ar', 'ca', 'cy', 'de', 'es', 'fr', 'ga', - 'hi', 'it', 'ja', 'oc', 'pt', 'ru', 'zh') + baseDir = os.getcwd() + languagesStr = getSupportedLanguages(baseDir) + assert languagesStr # load all translations into a dict langDict = {} @@ -2869,6 +3760,7 @@ def _testGetMentionedPeople() -> None: def _testReplyToPublicPost() -> None: baseDir = os.getcwd() + systemLanguage = 'en' nickname = 'test7492362' domain = 'other.site' port = 443 @@ -2890,6 +3782,8 @@ def _testReplyToPublicPost() -> None: testEventTime = None testLocation = None testIsArticle = False + conversationId = None + lowBandwidth = True reply = \ createPublicPost(baseDir, nickname, domain, port, httpPrefix, content, followersOnly, saveToFile, @@ -2899,25 +3793,25 @@ def _testReplyToPublicPost() -> None: testInReplyToAtomUri, testSubject, testSchedulePost, testEventDate, testEventTime, testLocation, - testIsArticle) + testIsArticle, systemLanguage, conversationId, + lowBandwidth) # print(str(reply)) assert reply['object']['content'] == \ '

' + \ '@ninjarodent' + \ ' This is a test.

' + reply['object']['contentMap'][systemLanguage] = reply['object']['content'] assert reply['object']['tag'][0]['type'] == 'Mention' assert reply['object']['tag'][0]['name'] == '@ninjarodent@rat.site' - assert reply['object']['tag'][0]['href'] == \ - 'https://rat.site/users/ninjarodent' + assert reply['object']['tag'][0]['href'] == 'https://rat.site/@ninjarodent' assert len(reply['object']['to']) == 1 assert reply['object']['to'][0].endswith('#Public') assert len(reply['object']['cc']) >= 1 assert reply['object']['cc'][0].endswith(nickname + '/followers') assert len(reply['object']['tag']) == 1 assert len(reply['object']['cc']) == 2 - assert reply['object']['cc'][1] == \ - httpPrefix + '://rat.site/users/ninjarodent' + assert reply['object']['cc'][1] == httpPrefix + '://rat.site/@ninjarodent' def _getFunctionCallArgs(name: str, lines: [], startLineCtr: int) -> []: @@ -3079,6 +3973,8 @@ def _testFunctions(): for sourceFile in files: if not sourceFile.endswith('.py'): continue + if sourceFile.startswith('.#'): + continue modName = sourceFile.replace('.py', '') modules[modName] = { 'functions': [] @@ -3240,6 +4136,8 @@ def _testFunctions(): 'str2bool', 'runNewswireDaemon', 'runNewswireWatchdog', + 'runFederatedSharesWatchdog', + 'runFederatedSharesDaemon', 'threadSendPost', 'sendToFollowers', 'expireCache', @@ -3251,6 +4149,7 @@ def _testFunctions(): 'getThisWeeksEvents', 'getAvailability', '_testThreadsFunction', + 'createServerGroup', 'createServerAlice', 'createServerBob', 'createServerEve', @@ -3391,6 +4290,7 @@ def _testFunctions(): def _testLinksWithinPost() -> None: baseDir = os.getcwd() + systemLanguage = 'en' nickname = 'test27636' domain = 'rando.site' port = 443 @@ -3413,6 +4313,8 @@ def _testLinksWithinPost() -> None: testEventTime = None testLocation = None testIsArticle = False + conversationId = None + lowBandwidth = True postJsonObject = \ createPublicPost(baseDir, nickname, domain, port, httpPrefix, @@ -3423,7 +4325,8 @@ def _testLinksWithinPost() -> None: testInReplyTo, testInReplyToAtomUri, testSubject, testSchedulePost, testEventDate, testEventTime, testLocation, - testIsArticle) + testIsArticle, systemLanguage, conversationId, + lowBandwidth) assert postJsonObject['object']['content'] == \ '

This is a test post with links.

' + \ @@ -3436,6 +4339,8 @@ def _testLinksWithinPost() -> None: 'rel="nofollow noopener noreferrer" target="_blank">' + \ '' + \ 'freedombone.net

' + assert postJsonObject['object']['content'] == \ + postJsonObject['object']['contentMap'][systemLanguage] content = "

Some text

Other text

More text

" + \ "
Errno::EOHNOES (No such file or rodent @ " + \
@@ -3457,8 +4362,10 @@ def _testLinksWithinPost() -> None:
                          testInReplyTo, testInReplyToAtomUri,
                          testSubject, testSchedulePost,
                          testEventDate, testEventTime, testLocation,
-                         testIsArticle)
+                         testIsArticle, systemLanguage, conversationId,
+                         lowBandwidth)
     assert postJsonObject['object']['content'] == content
+    assert postJsonObject['object']['contentMap'][systemLanguage] == content
 
 
 def _testMastoApi():
@@ -3815,6 +4722,7 @@ def _testRemovePostInteractions() -> None:
     assert postJsonObject['object']['shares'] == {}
     assert postJsonObject['object']['bookmarks'] == {}
     assert postJsonObject['object']['ignores'] == {}
+    postJsonObject['object']['to'] = ["some private address"]
     assert not removePostInteractions(postJsonObject, False)
 
 
@@ -4193,13 +5101,193 @@ def _testLimitRepetedWords() -> None:
     assert result == expected
 
 
+def _testSetActorLanguages():
+    print('testSetActorLanguages')
+    actorJson = {
+        "attachment": []
+    }
+    setActorLanguages(None, actorJson, 'es, fr, en')
+    assert len(actorJson['attachment']) == 1
+    assert actorJson['attachment'][0]['name'] == 'Languages'
+    assert actorJson['attachment'][0]['type'] == 'PropertyValue'
+    assert isinstance(actorJson['attachment'][0]['value'], str)
+    assert ',' in actorJson['attachment'][0]['value']
+    langList = getActorLanguagesList(actorJson)
+    assert 'en' in langList
+    assert 'fr' in langList
+    assert 'es' in langList
+    languagesStr = getActorLanguages(actorJson)
+    assert languagesStr == 'en / es / fr'
+
+
+def _testGetLinksFromContent():
+    print('testGetLinksFromContent')
+    content = 'This text has no links'
+    links = getLinksFromContent(content)
+    assert not links
+
+    link1 = 'https://somewebsite.net'
+    link2 = 'http://somewhere.or.other'
+    content = \
+        'This is @linked. ' + \
+        'And another.'
+    links = getLinksFromContent(content)
+    assert len(links.items()) == 2
+    assert links.get('@linked')
+    assert links['@linked'] == link1
+    assert links.get('another')
+    assert links['another'] == link2
+
+    contentPlain = '

' + removeHtml(content) + '

' + assert '>@linked' not in contentPlain + content = addLinksToContent(contentPlain, links) + assert '>@linked' in content + + +def _testAuthorizeSharedItems(): + print('testAuthorizeSharedItems') + sharedItemsFederatedDomains = \ + ['dog.domain', 'cat.domain', 'birb.domain'] + tokensJson = \ + generateSharedItemFederationTokens(sharedItemsFederatedDomains, None) + tokensJson = \ + createSharedItemFederationToken(None, 'cat.domain', False, tokensJson) + assert tokensJson + assert not tokensJson.get('dog.domain') + assert tokensJson.get('cat.domain') + assert not tokensJson.get('birb.domain') + assert len(tokensJson['dog.domain']) == 0 + assert len(tokensJson['cat.domain']) >= 64 + assert len(tokensJson['birb.domain']) == 0 + assert not authorizeSharedItems(sharedItemsFederatedDomains, None, + 'birb.domain', + 'cat.domain', 'M' * 86, + False, tokensJson) + assert authorizeSharedItems(sharedItemsFederatedDomains, None, + 'birb.domain', + 'cat.domain', tokensJson['cat.domain'], + False, tokensJson) + tokensJson = \ + updateSharedItemFederationToken(None, + 'dog.domain', 'testToken', + True, tokensJson) + assert tokensJson['dog.domain'] == 'testToken' + + # the shared item federation list changes + sharedItemsFederatedDomains = \ + ['possum.domain', 'cat.domain', 'birb.domain'] + tokensJson = mergeSharedItemTokens(None, '', + sharedItemsFederatedDomains, + tokensJson) + assert 'dog.domain' not in tokensJson + assert 'cat.domain' in tokensJson + assert len(tokensJson['cat.domain']) >= 64 + assert 'birb.domain' in tokensJson + assert 'possum.domain' in tokensJson + assert len(tokensJson['birb.domain']) == 0 + assert len(tokensJson['possum.domain']) == 0 + + +def _testDateConversions() -> None: + print('testDateConversions') + dateStr = "2021-05-16T14:37:41Z" + dateSec = dateStringToSeconds(dateStr) + dateStr2 = dateSecondsToString(dateSec) + assert dateStr == dateStr2 + + +def _testValidPassword(): + print('testValidPassword') + assert not validPassword('123') + assert not validPassword('') + assert validPassword('パスワード12345') + assert validPassword('测试密码12345') + assert validPassword('A!bc:defg1/234?56') + + +def _testGetPriceFromString() -> None: + print('testGetPriceFromString') + price, curr = getPriceFromString("5.23") + assert price == "5.23" + assert curr == "EUR" + price, curr = getPriceFromString("£7.36") + assert price == "7.36" + assert curr == "GBP" + price, curr = getPriceFromString("$10.63") + assert price == "10.63" + assert curr == "USD" + + +def _translateOntology() -> None: + baseDir = os.getcwd() + ontologyTypes = getCategoryTypes(baseDir) + url = 'https://translate.astian.org' + apiKey = None + ltLangList = libretranslateLanguages(url, apiKey) + baseDir = os.getcwd() + languagesStr = getSupportedLanguages(baseDir) + assert languagesStr + + for oType in ontologyTypes: + changed = False + filename = baseDir + '/ontology/' + oType + 'Types.json' + if not os.path.isfile(filename): + continue + ontologyJson = loadJson(filename) + if not ontologyJson: + continue + index = -1 + for item in ontologyJson['@graph']: + index += 1 + if "rdfs:label" not in item: + continue + englishStr = None + languagesFound = [] + for label in item["rdfs:label"]: + if '@language' not in label: + continue + languagesFound.append(label['@language']) + if '@value' not in label: + continue + if label['@language'] == 'en': + englishStr = label['@value'] + if not englishStr: + continue + for lang in languagesStr: + if lang not in languagesFound: + translatedStr = None + if url and lang in ltLangList: + translatedStr = \ + libretranslate(url, englishStr, 'en', lang, apiKey) + if not translatedStr: + translatedStr = englishStr + else: + translatedStr = translatedStr.replace('

', '') + translatedStr = translatedStr.replace('

', '') + ontologyJson['@graph'][index]["rdfs:label"].append({ + "@value": translatedStr, + "@language": lang + }) + changed = True + if not changed: + continue + saveJson(ontologyJson, filename + '.new') + + def runAllTests(): print('Running tests...') updateDefaultThemesList(os.getcwd()) + _translateOntology() + _testGetPriceFromString() + _testFunctions() + _testDateConversions() + _testAuthorizeSharedItems() + _testValidPassword() + _testGetLinksFromContent() + _testSetActorLanguages() _testLimitRepetedWords() _testLimitWordLengths() _testSwitchWords() - _testFunctions() _testUserAgentDomain() _testRoles() _testSkills() diff --git a/theme/blue/icons/publish.png b/theme/blue/icons/publish.png index 0fe148eea..f733dd937 100644 Binary files a/theme/blue/icons/publish.png and b/theme/blue/icons/publish.png differ diff --git a/theme/blue/icons/scope_blog.png b/theme/blue/icons/scope_blog.png index 0fe148eea..f733dd937 100644 Binary files a/theme/blue/icons/scope_blog.png and b/theme/blue/icons/scope_blog.png differ diff --git a/theme/blue/icons/scope_wanted.png b/theme/blue/icons/scope_wanted.png new file mode 100644 index 000000000..1c3427b4f Binary files /dev/null and b/theme/blue/icons/scope_wanted.png differ diff --git a/theme/debian/icons/dm.png b/theme/debian/icons/dm.png index c0493f82e..ca459e5dc 100644 Binary files a/theme/debian/icons/dm.png and b/theme/debian/icons/dm.png differ diff --git a/theme/debian/icons/links.png b/theme/debian/icons/links.png index 584d722f9..0bc37c730 100644 Binary files a/theme/debian/icons/links.png and b/theme/debian/icons/links.png differ diff --git a/theme/debian/icons/logorss.png b/theme/debian/icons/logorss.png index 6bcef0273..bcf6a4468 100644 Binary files a/theme/debian/icons/logorss.png and b/theme/debian/icons/logorss.png differ diff --git a/theme/debian/icons/newswire_favicon.ico b/theme/debian/icons/newswire_favicon.ico index a368686dc..b2b79a9d4 100644 Binary files a/theme/debian/icons/newswire_favicon.ico and b/theme/debian/icons/newswire_favicon.ico differ diff --git a/theme/debian/icons/publish.png b/theme/debian/icons/publish.png index 0fe148eea..13c55f0b8 100644 Binary files a/theme/debian/icons/publish.png and b/theme/debian/icons/publish.png differ diff --git a/theme/debian/icons/scope_blog.png b/theme/debian/icons/scope_blog.png index e3cdb1b81..f733dd937 100644 Binary files a/theme/debian/icons/scope_blog.png and b/theme/debian/icons/scope_blog.png differ diff --git a/theme/debian/icons/scope_followers.png b/theme/debian/icons/scope_followers.png index 2e420954c..aada82f0a 100644 Binary files a/theme/debian/icons/scope_followers.png and b/theme/debian/icons/scope_followers.png differ diff --git a/theme/debian/icons/scope_reminder.png b/theme/debian/icons/scope_reminder.png index 809376840..739ff6d1d 100644 Binary files a/theme/debian/icons/scope_reminder.png and b/theme/debian/icons/scope_reminder.png differ diff --git a/theme/debian/icons/scope_share.png b/theme/debian/icons/scope_share.png index 07fe95502..42a1c185b 100644 Binary files a/theme/debian/icons/scope_share.png and b/theme/debian/icons/scope_share.png differ diff --git a/theme/debian/icons/scope_unlisted.png b/theme/debian/icons/scope_unlisted.png index b3ce02e69..b1aa70e87 100644 Binary files a/theme/debian/icons/scope_unlisted.png and b/theme/debian/icons/scope_unlisted.png differ diff --git a/theme/debian/icons/scope_wanted.png b/theme/debian/icons/scope_wanted.png new file mode 100644 index 000000000..1c3427b4f Binary files /dev/null and b/theme/debian/icons/scope_wanted.png differ diff --git a/theme/debian/theme.json b/theme/debian/theme.json index 50bfcd2ce..27cd7fd91 100644 --- a/theme/debian/theme.json +++ b/theme/debian/theme.json @@ -14,7 +14,7 @@ "post-separator-margin-top": "10px", "post-separator-margin-bottom": "10px", "vertical-between-posts": "10px", - "time-vertical-align": "10px", + "time-vertical-align": "0%", "button-corner-radius": "5px", "timeline-border-radius": "5px", "newswire-publish-icon": "True", diff --git a/theme/default/icons/dm.png b/theme/default/icons/dm.png index 78d90ae76..ca459e5dc 100644 Binary files a/theme/default/icons/dm.png and b/theme/default/icons/dm.png differ diff --git a/theme/default/icons/links.png b/theme/default/icons/links.png index 4ef5fe6a4..0bc37c730 100644 Binary files a/theme/default/icons/links.png and b/theme/default/icons/links.png differ diff --git a/theme/default/icons/logorss.png b/theme/default/icons/logorss.png index c5fad44cb..d30aec824 100644 Binary files a/theme/default/icons/logorss.png and b/theme/default/icons/logorss.png differ diff --git a/theme/default/icons/newswire_favicon.ico b/theme/default/icons/newswire_favicon.ico index a368686dc..b2b79a9d4 100644 Binary files a/theme/default/icons/newswire_favicon.ico and b/theme/default/icons/newswire_favicon.ico differ diff --git a/theme/default/icons/publish.png b/theme/default/icons/publish.png index 0fe148eea..13c55f0b8 100644 Binary files a/theme/default/icons/publish.png and b/theme/default/icons/publish.png differ diff --git a/theme/default/icons/scope_blog.png b/theme/default/icons/scope_blog.png index 0fe148eea..f733dd937 100644 Binary files a/theme/default/icons/scope_blog.png and b/theme/default/icons/scope_blog.png differ diff --git a/theme/default/icons/scope_followers.png b/theme/default/icons/scope_followers.png index 2e420954c..aada82f0a 100644 Binary files a/theme/default/icons/scope_followers.png and b/theme/default/icons/scope_followers.png differ diff --git a/theme/default/icons/scope_reminder.png b/theme/default/icons/scope_reminder.png index 809376840..739ff6d1d 100644 Binary files a/theme/default/icons/scope_reminder.png and b/theme/default/icons/scope_reminder.png differ diff --git a/theme/default/icons/scope_share.png b/theme/default/icons/scope_share.png index 07fe95502..42a1c185b 100644 Binary files a/theme/default/icons/scope_share.png and b/theme/default/icons/scope_share.png differ diff --git a/theme/default/icons/scope_unlisted.png b/theme/default/icons/scope_unlisted.png index b3ce02e69..b1aa70e87 100644 Binary files a/theme/default/icons/scope_unlisted.png and b/theme/default/icons/scope_unlisted.png differ diff --git a/theme/default/icons/scope_wanted.png b/theme/default/icons/scope_wanted.png new file mode 100644 index 000000000..1c3427b4f Binary files /dev/null and b/theme/default/icons/scope_wanted.png differ diff --git a/theme/hacker/icons/add.png b/theme/hacker/icons/add.png index 3b1e726c0..a8d7ed561 100644 Binary files a/theme/hacker/icons/add.png and b/theme/hacker/icons/add.png differ diff --git a/theme/hacker/icons/avatar_default.png b/theme/hacker/icons/avatar_default.png index 9c50078f5..cb882cc83 100644 Binary files a/theme/hacker/icons/avatar_default.png and b/theme/hacker/icons/avatar_default.png differ diff --git a/theme/hacker/icons/avatar_news.png b/theme/hacker/icons/avatar_news.png index 0b5b4dd49..04732035b 100644 Binary files a/theme/hacker/icons/avatar_news.png and b/theme/hacker/icons/avatar_news.png differ diff --git a/theme/hacker/icons/calendar.png b/theme/hacker/icons/calendar.png index c7c0bdd15..9284d9162 100644 Binary files a/theme/hacker/icons/calendar.png and b/theme/hacker/icons/calendar.png differ diff --git a/theme/hacker/icons/calendar_notify.png b/theme/hacker/icons/calendar_notify.png index 8b94b0e08..c1ca09ea8 100644 Binary files a/theme/hacker/icons/calendar_notify.png and b/theme/hacker/icons/calendar_notify.png differ diff --git a/theme/hacker/icons/delete.png b/theme/hacker/icons/delete.png index 5ac4a3fb7..01c4e876d 100644 Binary files a/theme/hacker/icons/delete.png and b/theme/hacker/icons/delete.png differ diff --git a/theme/hacker/icons/dm.png b/theme/hacker/icons/dm.png index 5e1331675..13bb2224a 100644 Binary files a/theme/hacker/icons/dm.png and b/theme/hacker/icons/dm.png differ diff --git a/theme/hacker/icons/edit.png b/theme/hacker/icons/edit.png index ff6ade968..214f49cf4 100644 Binary files a/theme/hacker/icons/edit.png and b/theme/hacker/icons/edit.png differ diff --git a/theme/hacker/icons/edit_notify.png b/theme/hacker/icons/edit_notify.png index e0a5e991f..232b91139 100644 Binary files a/theme/hacker/icons/edit_notify.png and b/theme/hacker/icons/edit_notify.png differ diff --git a/theme/hacker/icons/favicon.ico b/theme/hacker/icons/favicon.ico index e04365761..612635e5a 100644 Binary files a/theme/hacker/icons/favicon.ico and b/theme/hacker/icons/favicon.ico differ diff --git a/theme/hacker/icons/links.png b/theme/hacker/icons/links.png index dc23017ba..afde0b765 100644 Binary files a/theme/hacker/icons/links.png and b/theme/hacker/icons/links.png differ diff --git a/theme/hacker/icons/logorss.png b/theme/hacker/icons/logorss.png index a8eff40dd..7ca5e7dbe 100644 Binary files a/theme/hacker/icons/logorss.png and b/theme/hacker/icons/logorss.png differ diff --git a/theme/hacker/icons/mute.png b/theme/hacker/icons/mute.png index cce158ecf..ad9da3a65 100644 Binary files a/theme/hacker/icons/mute.png and b/theme/hacker/icons/mute.png differ diff --git a/theme/hacker/icons/newpost.png b/theme/hacker/icons/newpost.png index e69bbb9f0..f31f7ffdf 100644 Binary files a/theme/hacker/icons/newpost.png and b/theme/hacker/icons/newpost.png differ diff --git a/theme/hacker/icons/newswire_favicon.ico b/theme/hacker/icons/newswire_favicon.ico index 501ffe048..e1050fe5a 100644 Binary files a/theme/hacker/icons/newswire_favicon.ico and b/theme/hacker/icons/newswire_favicon.ico differ diff --git a/theme/hacker/icons/pagedown.png b/theme/hacker/icons/pagedown.png index 4eb89a48f..8db755cae 100644 Binary files a/theme/hacker/icons/pagedown.png and b/theme/hacker/icons/pagedown.png differ diff --git a/theme/hacker/icons/pageup.png b/theme/hacker/icons/pageup.png index 06140dfdc..9dbc17214 100644 Binary files a/theme/hacker/icons/pageup.png and b/theme/hacker/icons/pageup.png differ diff --git a/theme/hacker/icons/person.png b/theme/hacker/icons/person.png index 09048353d..d9b64a94f 100644 Binary files a/theme/hacker/icons/person.png and b/theme/hacker/icons/person.png differ diff --git a/theme/hacker/icons/prev.png b/theme/hacker/icons/prev.png index f9c50964c..91aca8e1b 100644 Binary files a/theme/hacker/icons/prev.png and b/theme/hacker/icons/prev.png differ diff --git a/theme/hacker/icons/publish.png b/theme/hacker/icons/publish.png index 194180c27..f003503da 100644 Binary files a/theme/hacker/icons/publish.png and b/theme/hacker/icons/publish.png differ diff --git a/theme/hacker/icons/repeat.png b/theme/hacker/icons/repeat.png index af5bd8f34..a0cb077a8 100644 Binary files a/theme/hacker/icons/repeat.png and b/theme/hacker/icons/repeat.png differ diff --git a/theme/hacker/icons/repeat_inactive.png b/theme/hacker/icons/repeat_inactive.png index e9da71aa3..fda04f921 100644 Binary files a/theme/hacker/icons/repeat_inactive.png and b/theme/hacker/icons/repeat_inactive.png differ diff --git a/theme/hacker/icons/reply.png b/theme/hacker/icons/reply.png index 1cc0ac457..8946f6eb6 100644 Binary files a/theme/hacker/icons/reply.png and b/theme/hacker/icons/reply.png differ diff --git a/theme/hacker/icons/scope_blog.png b/theme/hacker/icons/scope_blog.png index cbeec5c3c..95658d810 100644 Binary files a/theme/hacker/icons/scope_blog.png and b/theme/hacker/icons/scope_blog.png differ diff --git a/theme/hacker/icons/scope_dm.png b/theme/hacker/icons/scope_dm.png index 160f7c2c8..82cf9913c 100644 Binary files a/theme/hacker/icons/scope_dm.png and b/theme/hacker/icons/scope_dm.png differ diff --git a/theme/hacker/icons/scope_event.png b/theme/hacker/icons/scope_event.png index c7c0bdd15..80b2ad91c 100644 Binary files a/theme/hacker/icons/scope_event.png and b/theme/hacker/icons/scope_event.png differ diff --git a/theme/hacker/icons/scope_followers.png b/theme/hacker/icons/scope_followers.png index 4f0854aa1..84704f489 100644 Binary files a/theme/hacker/icons/scope_followers.png and b/theme/hacker/icons/scope_followers.png differ diff --git a/theme/hacker/icons/scope_reminder.png b/theme/hacker/icons/scope_reminder.png index 644071bd0..7cad74582 100644 Binary files a/theme/hacker/icons/scope_reminder.png and b/theme/hacker/icons/scope_reminder.png differ diff --git a/theme/hacker/icons/scope_share.png b/theme/hacker/icons/scope_share.png index d44b708e4..0707e7597 100644 Binary files a/theme/hacker/icons/scope_share.png and b/theme/hacker/icons/scope_share.png differ diff --git a/theme/hacker/icons/scope_unlisted.png b/theme/hacker/icons/scope_unlisted.png index 34d5407ed..3f630ed3d 100644 Binary files a/theme/hacker/icons/scope_unlisted.png and b/theme/hacker/icons/scope_unlisted.png differ diff --git a/theme/hacker/icons/scope_wanted.png b/theme/hacker/icons/scope_wanted.png new file mode 100644 index 000000000..b32bc0a57 Binary files /dev/null and b/theme/hacker/icons/scope_wanted.png differ diff --git a/theme/hacker/icons/showhide.png b/theme/hacker/icons/showhide.png index 082225dae..f67d38daf 100644 Binary files a/theme/hacker/icons/showhide.png and b/theme/hacker/icons/showhide.png differ diff --git a/theme/hacker/icons/unmute.png b/theme/hacker/icons/unmute.png index 90414d7ea..ee5fddf02 100644 Binary files a/theme/hacker/icons/unmute.png and b/theme/hacker/icons/unmute.png differ diff --git a/theme/hacker/theme.json b/theme/hacker/theme.json index 14f6161c0..6bc48bcc5 100644 --- a/theme/hacker/theme.json +++ b/theme/hacker/theme.json @@ -1,4 +1,5 @@ { + "column-left-header-background": "#035103", "font-size-header": "12px", "font-size-header-mobile": "20px", "font-size-button-mobile": "20px", @@ -7,7 +8,7 @@ "font-size-newswire": "16px", "font-size-newswire-mobile": "36px", "font-size-dropdown-header": "26px", - "font-size-mobile": "20px", + "font-size-mobile": "40px", "font-size": "26px", "font-size2": "16px", "font-size3": "36px", diff --git a/theme/henge/icons/publish.png b/theme/henge/icons/publish.png index 739ab9fa9..e41fbaa04 100644 Binary files a/theme/henge/icons/publish.png and b/theme/henge/icons/publish.png differ diff --git a/theme/henge/icons/scope_blog.png b/theme/henge/icons/scope_blog.png index 739ab9fa9..2b399993d 100644 Binary files a/theme/henge/icons/scope_blog.png and b/theme/henge/icons/scope_blog.png differ diff --git a/theme/henge/icons/scope_wanted.png b/theme/henge/icons/scope_wanted.png new file mode 100644 index 000000000..765c300ad Binary files /dev/null and b/theme/henge/icons/scope_wanted.png differ diff --git a/theme/indymediaclassic/icons/publish.png b/theme/indymediaclassic/icons/publish.png index 17af02e86..6f6a2f15b 100644 Binary files a/theme/indymediaclassic/icons/publish.png and b/theme/indymediaclassic/icons/publish.png differ diff --git a/theme/indymediaclassic/icons/scope_blog.png b/theme/indymediaclassic/icons/scope_blog.png index 17af02e86..1f4aa4cb0 100644 Binary files a/theme/indymediaclassic/icons/scope_blog.png and b/theme/indymediaclassic/icons/scope_blog.png differ diff --git a/theme/indymediaclassic/icons/scope_wanted.png b/theme/indymediaclassic/icons/scope_wanted.png new file mode 100644 index 000000000..0cad13140 Binary files /dev/null and b/theme/indymediaclassic/icons/scope_wanted.png differ diff --git a/theme/indymediamodern/icons/publish.png b/theme/indymediamodern/icons/publish.png index 2917ae876..88d422a5d 100644 Binary files a/theme/indymediamodern/icons/publish.png and b/theme/indymediamodern/icons/publish.png differ diff --git a/theme/indymediamodern/icons/scope_blog.png b/theme/indymediamodern/icons/scope_blog.png index 15d9fbc06..47f1dd77f 100644 Binary files a/theme/indymediamodern/icons/scope_blog.png and b/theme/indymediamodern/icons/scope_blog.png differ diff --git a/theme/indymediamodern/icons/scope_wanted.png b/theme/indymediamodern/icons/scope_wanted.png new file mode 100644 index 000000000..1f3ef27b9 Binary files /dev/null and b/theme/indymediamodern/icons/scope_wanted.png differ diff --git a/theme/lcd/icons/publish.png b/theme/lcd/icons/publish.png index b26e97d50..c1065db11 100644 Binary files a/theme/lcd/icons/publish.png and b/theme/lcd/icons/publish.png differ diff --git a/theme/lcd/icons/scope_blog.png b/theme/lcd/icons/scope_blog.png index b26e97d50..46fa04299 100644 Binary files a/theme/lcd/icons/scope_blog.png and b/theme/lcd/icons/scope_blog.png differ diff --git a/theme/lcd/icons/scope_wanted.png b/theme/lcd/icons/scope_wanted.png new file mode 100644 index 000000000..3ade121a4 Binary files /dev/null and b/theme/lcd/icons/scope_wanted.png differ diff --git a/theme/light/icons/links.png b/theme/light/icons/links.png index 0a18048fc..15bcd611b 100644 Binary files a/theme/light/icons/links.png and b/theme/light/icons/links.png differ diff --git a/theme/light/icons/logorss.png b/theme/light/icons/logorss.png index 62c9a2fe0..e651b93de 100644 Binary files a/theme/light/icons/logorss.png and b/theme/light/icons/logorss.png differ diff --git a/theme/light/icons/newswire_favicon.ico b/theme/light/icons/newswire_favicon.ico index a368686dc..e9a67c9c8 100644 Binary files a/theme/light/icons/newswire_favicon.ico and b/theme/light/icons/newswire_favicon.ico differ diff --git a/theme/light/icons/publish.png b/theme/light/icons/publish.png index 5f6e45a14..47b181df5 100644 Binary files a/theme/light/icons/publish.png and b/theme/light/icons/publish.png differ diff --git a/theme/light/icons/scope_blog.png b/theme/light/icons/scope_blog.png index cc063d0fb..a8745114a 100644 Binary files a/theme/light/icons/scope_blog.png and b/theme/light/icons/scope_blog.png differ diff --git a/theme/light/icons/scope_followers.png b/theme/light/icons/scope_followers.png index efd3353e6..51c32c8f9 100644 Binary files a/theme/light/icons/scope_followers.png and b/theme/light/icons/scope_followers.png differ diff --git a/theme/light/icons/scope_reminder.png b/theme/light/icons/scope_reminder.png index 12d055dee..75d27032a 100644 Binary files a/theme/light/icons/scope_reminder.png and b/theme/light/icons/scope_reminder.png differ diff --git a/theme/light/icons/scope_share.png b/theme/light/icons/scope_share.png index 1cdd0b2a6..c0b0a7404 100644 Binary files a/theme/light/icons/scope_share.png and b/theme/light/icons/scope_share.png differ diff --git a/theme/light/icons/scope_unlisted.png b/theme/light/icons/scope_unlisted.png index ca84e23c3..a0e8bae57 100644 Binary files a/theme/light/icons/scope_unlisted.png and b/theme/light/icons/scope_unlisted.png differ diff --git a/theme/light/icons/scope_wanted.png b/theme/light/icons/scope_wanted.png new file mode 100644 index 000000000..ff3f73587 Binary files /dev/null and b/theme/light/icons/scope_wanted.png differ diff --git a/theme/night/icons/like_inactive.png b/theme/night/icons/like_inactive.png index dbbe24f5f..d101e48d2 100644 Binary files a/theme/night/icons/like_inactive.png and b/theme/night/icons/like_inactive.png differ diff --git a/theme/night/icons/links.png b/theme/night/icons/links.png index 4ef5fe6a4..0bc37c730 100644 Binary files a/theme/night/icons/links.png and b/theme/night/icons/links.png differ diff --git a/theme/night/icons/logorss.png b/theme/night/icons/logorss.png index 1490090e2..368bd50d9 100644 Binary files a/theme/night/icons/logorss.png and b/theme/night/icons/logorss.png differ diff --git a/theme/night/icons/newswire_favicon.ico b/theme/night/icons/newswire_favicon.ico index a368686dc..6e3efe2c9 100644 Binary files a/theme/night/icons/newswire_favicon.ico and b/theme/night/icons/newswire_favicon.ico differ diff --git a/theme/night/icons/publish.png b/theme/night/icons/publish.png index 0fe148eea..13c55f0b8 100644 Binary files a/theme/night/icons/publish.png and b/theme/night/icons/publish.png differ diff --git a/theme/night/icons/scope_blog.png b/theme/night/icons/scope_blog.png index e3cdb1b81..f733dd937 100644 Binary files a/theme/night/icons/scope_blog.png and b/theme/night/icons/scope_blog.png differ diff --git a/theme/night/icons/scope_followers.png b/theme/night/icons/scope_followers.png index 2e420954c..aada82f0a 100644 Binary files a/theme/night/icons/scope_followers.png and b/theme/night/icons/scope_followers.png differ diff --git a/theme/night/icons/scope_reminder.png b/theme/night/icons/scope_reminder.png index 809376840..739ff6d1d 100644 Binary files a/theme/night/icons/scope_reminder.png and b/theme/night/icons/scope_reminder.png differ diff --git a/theme/night/icons/scope_share.png b/theme/night/icons/scope_share.png index 07fe95502..42a1c185b 100644 Binary files a/theme/night/icons/scope_share.png and b/theme/night/icons/scope_share.png differ diff --git a/theme/night/icons/scope_unlisted.png b/theme/night/icons/scope_unlisted.png index b3ce02e69..b1aa70e87 100644 Binary files a/theme/night/icons/scope_unlisted.png and b/theme/night/icons/scope_unlisted.png differ diff --git a/theme/night/icons/scope_wanted.png b/theme/night/icons/scope_wanted.png new file mode 100644 index 000000000..84e56d2fd Binary files /dev/null and b/theme/night/icons/scope_wanted.png differ diff --git a/theme/pixel/icons/scope_wanted.png b/theme/pixel/icons/scope_wanted.png new file mode 100644 index 000000000..a6cf58e7c Binary files /dev/null and b/theme/pixel/icons/scope_wanted.png differ diff --git a/theme/purple/icons/publish.png b/theme/purple/icons/publish.png index d918e4f72..9694a0fda 100644 Binary files a/theme/purple/icons/publish.png and b/theme/purple/icons/publish.png differ diff --git a/theme/purple/icons/scope_blog.png b/theme/purple/icons/scope_blog.png index d918e4f72..10e0f4a11 100644 Binary files a/theme/purple/icons/scope_blog.png and b/theme/purple/icons/scope_blog.png differ diff --git a/theme/purple/icons/scope_wanted.png b/theme/purple/icons/scope_wanted.png new file mode 100644 index 000000000..cf04b1d0e Binary files /dev/null and b/theme/purple/icons/scope_wanted.png differ diff --git a/theme/rc3/icons/publish.png b/theme/rc3/icons/publish.png index 3c3ec5031..0514068b9 100644 Binary files a/theme/rc3/icons/publish.png and b/theme/rc3/icons/publish.png differ diff --git a/theme/rc3/icons/scope_blog.png b/theme/rc3/icons/scope_blog.png index 376c54f30..389e341ed 100644 Binary files a/theme/rc3/icons/scope_blog.png and b/theme/rc3/icons/scope_blog.png differ diff --git a/theme/rc3/icons/scope_wanted.png b/theme/rc3/icons/scope_wanted.png new file mode 100644 index 000000000..7c12e9532 Binary files /dev/null and b/theme/rc3/icons/scope_wanted.png differ diff --git a/theme/solidaric/icons/scope_wanted.png b/theme/solidaric/icons/scope_wanted.png new file mode 100644 index 000000000..4c5f16a2c Binary files /dev/null and b/theme/solidaric/icons/scope_wanted.png differ diff --git a/theme/starlight/icons/dm.png b/theme/starlight/icons/dm.png index dcbba2b09..3d4e7399e 100644 Binary files a/theme/starlight/icons/dm.png and b/theme/starlight/icons/dm.png differ diff --git a/theme/starlight/icons/links.png b/theme/starlight/icons/links.png index 577d59673..84e330db2 100644 Binary files a/theme/starlight/icons/links.png and b/theme/starlight/icons/links.png differ diff --git a/theme/starlight/icons/logorss.png b/theme/starlight/icons/logorss.png index 90ab9a7e8..7d7666eb0 100644 Binary files a/theme/starlight/icons/logorss.png and b/theme/starlight/icons/logorss.png differ diff --git a/theme/starlight/icons/publish.png b/theme/starlight/icons/publish.png index bfcfbb0b6..008dda604 100644 Binary files a/theme/starlight/icons/publish.png and b/theme/starlight/icons/publish.png differ diff --git a/theme/starlight/icons/scope_blog.png b/theme/starlight/icons/scope_blog.png index bfcfbb0b6..ed0bcfafb 100644 Binary files a/theme/starlight/icons/scope_blog.png and b/theme/starlight/icons/scope_blog.png differ diff --git a/theme/starlight/icons/scope_wanted.png b/theme/starlight/icons/scope_wanted.png new file mode 100644 index 000000000..afdc98c2c Binary files /dev/null and b/theme/starlight/icons/scope_wanted.png differ diff --git a/theme/zen/icons/newswire_favicon.ico b/theme/zen/icons/newswire_favicon.ico index 09a2d9963..c1812c8bd 100644 Binary files a/theme/zen/icons/newswire_favicon.ico and b/theme/zen/icons/newswire_favicon.ico differ diff --git a/theme/zen/icons/publish.png b/theme/zen/icons/publish.png index 37fdf5888..c6f64cbe1 100644 Binary files a/theme/zen/icons/publish.png and b/theme/zen/icons/publish.png differ diff --git a/theme/zen/icons/scope_blog.png b/theme/zen/icons/scope_blog.png index 71a533713..475cdb1ea 100644 Binary files a/theme/zen/icons/scope_blog.png and b/theme/zen/icons/scope_blog.png differ diff --git a/theme/zen/icons/scope_wanted.png b/theme/zen/icons/scope_wanted.png new file mode 100644 index 000000000..c20680524 Binary files /dev/null and b/theme/zen/icons/scope_wanted.png differ diff --git a/translations/ar.json b/translations/ar.json index 50cca5a13..9523f6978 100644 --- a/translations/ar.json +++ b/translations/ar.json @@ -90,7 +90,7 @@ "View": "رأي", "Stop blocking": "وقف الحظر", "Enter an emoji name to search for": "أدخل اسم رمز تعبيري للبحث عنه", - "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "أدخل عنوانًا أو عنصرًا مشتركًا أو! history أو #hashtag أو * مهارة أو: emoji: للبحث عنه", + "Search screen text": "أدخل عنوانًا ، أو عنصرًا مشتركًا ، أو -حفظ ، أو السجل ، أو #hashtag ، أو * مهارة ، أو مطلوبًا ، أو: رمز تعبيري: للبحث عن", "Go Back": "◀", "Moderation Information": "معلومات الاعتدال", "Suspended accounts": "الحسابات المعلقه", @@ -107,7 +107,7 @@ "Nickname": "كنية", "Enter Nickname": "أدخل كنية", "Password": "كلمه السر", - "Enter Password": "أدخل كلمة المرور", + "Enter Password": "الحد الأدنى 8 أحرف", "Profile for": "الملف الشخصي ل", "The files attached below should be no larger than 10MB in total uploaded at once.": "يجب ألا يتجاوز حجم الملفات المرفقة أدناه 10 ميغابايت في المجموع.", "Avatar image": "الصورة الرمزية", @@ -450,5 +450,29 @@ "Export Theme": "موضوع التصدير", "Custom post submit button text": "عرف نشر إرسال نص زر", "Blocked User Agents": "عوامل المستخدم المحظورة", - "Notify me when this account posts": "أعلمني عندما ينشر الحساب هذا" + "Notify me when this account posts": "أعلمني عندما ينشر الحساب هذا", + "Languages": "اللغات", + "Translated": "تترجم", + "Quantity": "كمية", + "food": "غذاء", + "Price": "السعر", + "Currency": "عملة", + "List of domains which can access the shared items catalog": "قائمة المجالات التي يمكن الوصول إلى كتالوج البنود المشتركة", + "Shares Catalog": "كتالوج الأسهم", + "tool": "أداة", + "clothes": "ملابس", + "medical": "طبي", + "Wanted": "مطلوب", + "Describe something wanted": "وصف شيء مطلوب", + "Enter the details for your wanted item below.": "أدخل تفاصيل العنصر المطلوب أدناه.", + "Name of the wanted item": "اسم العنصر المطلوب", + "Description of the item wanted": "وصف العنصر المطلوب", + "Type of wanted item. eg. hat": "نوع الشيء المطلوب. على سبيل المثال قبعة", + "Category of wanted item. eg. clothes": "فئة العنصر المطلوب. على سبيل المثال ملابس", + "City or location of the wanted item": "مدينة أو موقع العنصر المطلوب", + "Maximum Price": "السعر الأقصى", + "Create a new wanted item": "قم بإنشاء عنصر مطلوب جديد", + "Wanted Items Search": "البحث عن العناصر المطلوبة", + "Website": "موقع إلكتروني", + "Low Bandwidth": "انخفاض النطاق الترددي" } diff --git a/translations/ca.json b/translations/ca.json index c9a15be88..f44884e41 100644 --- a/translations/ca.json +++ b/translations/ca.json @@ -90,7 +90,7 @@ "View": "Veure", "Stop blocking": "Deixeu de bloquejar", "Enter an emoji name to search for": "Introduïu un nom emoji per cercar", - "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "Introduïu una adreça, un element compartit, un historial!, #Hashtag, * skill o: emoji: per cercar", + "Search screen text": "Introduïu una adreça, un element compartit, -guardar, 'història, #etiqueta, *habilitat, .volia o :emoji: per cercar", "Go Back": "◀", "Moderation Information": "Informació de moderació", "Suspended accounts": "Comptes suspesos", @@ -107,7 +107,7 @@ "Nickname": "Sobrenom", "Enter Nickname": "Introduïu el sobrenom", "Password": "Contrasenya", - "Enter Password": "Introduir la contrasenya", + "Enter Password": "Mínim 8 caràcters", "Profile for": "Perfil de", "The files attached below should be no larger than 10MB in total uploaded at once.": "Els fitxers adjunts a continuació no han de ser superior a 10 MB en total carregats alhora.", "Avatar image": "Imatge de Avatar", @@ -450,5 +450,29 @@ "Export Theme": "Tema d'exportació", "Custom post submit button text": "Text de botó d'enviament de publicacions personalitzades", "Blocked User Agents": "Agents d'usuari bloquejats", - "Notify me when this account posts": "Aviseu-me quan publiqui aquest compte" + "Notify me when this account posts": "Aviseu-me quan publiqui aquest compte", + "Languages": "Idiomes", + "Translated": "Traduït", + "Quantity": "Quantitat", + "food": "menjar", + "Price": "Preu", + "Currency": "Moneda", + "List of domains which can access the shared items catalog": "Llista de dominis que poden accedir al catàleg d'articles compartits", + "Shares Catalog": "Catàleg d'accions", + "tool": "eina", + "clothes": "roba", + "medical": "mèdic", + "Wanted": "Volia", + "Describe something wanted": "Descriviu alguna cosa que volgués", + "Enter the details for your wanted item below.": "Introduïu els detalls de l'article que voleu a continuació.", + "Name of the wanted item": "Nom de l'element desitjat", + "Description of the item wanted": "Descripció de l'element desitjat", + "Type of wanted item. eg. hat": "Tipus d'article desitjat. per exemple. barret", + "Category of wanted item. eg. clothes": "Categoria de l'article desitjat. per exemple. roba", + "City or location of the wanted item": "Ciutat o ubicació de l’article desitjat", + "Maximum Price": "Preu màxim", + "Create a new wanted item": "Creeu un element desitjat", + "Wanted Items Search": "Cerca d'articles desitjats", + "Website": "Lloc web", + "Low Bandwidth": "Ample de banda baixa" } diff --git a/translations/cy.json b/translations/cy.json index 0cf0e9980..6f761d3e9 100644 --- a/translations/cy.json +++ b/translations/cy.json @@ -90,7 +90,7 @@ "View": "Gweld", "Stop blocking": "Stopiwch rwystro", "Enter an emoji name to search for": "Rhowch enw emoji i chwilio amdano", - "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "Rhowch gyfeiriad, eitem a rennir ,! Hanes, #hashtag, * sgil neu: emoji: i chwilio amdano", + "Search screen text": "Rhowch gyfeiriad, eitem a rennir, -arbed, 'hanes, #hashnod, *sgil, .eisiau neu :emoji: i chwilio am", "Go Back": "◀", "Moderation Information": "Gwybodaeth Cymedroli", "Suspended accounts": "Cyfrifon gohiriedig", @@ -107,7 +107,7 @@ "Nickname": "Llysenw", "Enter Nickname": "Rhowch Llysenw", "Password": "Cyfrinair", - "Enter Password": "Rhowch Gyfrinair", + "Enter Password": "O leiaf 8 cymeriad", "Profile for": "Proffil ar gyfer", "The files attached below should be no larger than 10MB in total uploaded at once.": "Ni ddylai'r ffeiliau sydd ynghlwm isod fod yn fwy na 10MB wedi'u llwytho i fyny ar unwaith.", "Avatar image": "Delwedd Avatar", @@ -450,5 +450,29 @@ "Export Theme": "Thema Allforio", "Custom post submit button text": "Testun Post Post Post", "Blocked User Agents": "Asiantau defnyddwyr wedi'u blocio", - "Notify me when this account posts": "Rhoi gwybod i mi pan fydd y cyfrifon cyfrif hwn" + "Notify me when this account posts": "Rhoi gwybod i mi pan fydd y cyfrifon cyfrif hwn", + "Languages": "Ieithoedd", + "Translated": "Chyfieithwyd", + "Quantity": "Symiau", + "food": "bwyd", + "Price": "Prisia", + "Currency": "Harian", + "List of domains which can access the shared items catalog": "Rhestr o barthau a all gael mynediad i'r catalog eitemau a rennir", + "Shares Catalog": "Catalog Cyfranddaliadau", + "tool": "hofferyn", + "clothes": "ddillad", + "medical": "meddygol", + "Wanted": "Am", + "Describe something wanted": "Disgrifio rhywbeth ei eisiau", + "Enter the details for your wanted item below.": "Rhowch y manylion ar gyfer eich eitem eisiau isod.", + "Name of the wanted item": "Enw'r eitem sydd ei eisiau", + "Description of the item wanted": "Disgrifiad o'r eitem eisiau", + "Type of wanted item. eg. hat": "Math o eitem eisiau. ee. het", + "Category of wanted item. eg. clothes": "Categori yr eitem sydd ei eisiau. ee. dillad", + "City or location of the wanted item": "Dinas neu leoliad yr eitem sydd ei eisiau", + "Maximum Price": "Uchafswm Pris", + "Create a new wanted item": "Creu eitem newydd ei heisiau", + "Wanted Items Search": "Chwilio Eitemau Eisiau", + "Website": "Gwefan", + "Low Bandwidth": "Lled band isel" } diff --git a/translations/de.json b/translations/de.json index df10a2e6e..d7d3dd56b 100644 --- a/translations/de.json +++ b/translations/de.json @@ -90,7 +90,7 @@ "View": "Zeigen", "Stop blocking": "Sperre aufheben", "Enter an emoji name to search for": "Geben Sie einen Emojinamen ein, nach dem gesucht werden soll", - "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "Geben Sie eine Adresse, ein freigegebenes Element ,! History, #hashtag, * Skill oder: emoji: ein, nach der gesucht werden soll", + "Search screen text": "Geben Sie eine Adresse, ein geteiltes Element, -Speichern, 'Verlauf, #Hashtag, *Fähigkeit, .gesucht oder :emoji: ein, um nach zu suchen", "Go Back": "◀", "Moderation Information": "Moderationsinformationen", "Suspended accounts": "Temporäre gesperrte Benutzer", @@ -107,7 +107,7 @@ "Nickname": "Benutzername", "Enter Nickname": "Benutzername eingeben", "Password": "Passwort", - "Enter Password": "Passwort eingeben", + "Enter Password": "Mindestens 8 Zeichen", "Profile for": "Profil für", "The files attached below should be no larger than 10MB in total uploaded at once.": "Die unten angehängten Dateien sollten gemeinsam pro Upload nicht größer als 10 MB sein.", "Avatar image": "Avatar-Bild", @@ -450,5 +450,29 @@ "Export Theme": "Theme exportieren", "Custom post submit button text": "Benutzerdefinierte Post-Senden Schaltfläche Text", "Blocked User Agents": "Blockierte Benutzeragenten", - "Notify me when this account posts": "Benachrichtigen Sie mich, wenn dieses Konto postet" + "Notify me when this account posts": "Benachrichtigen Sie mich, wenn dieses Konto postet", + "Languages": "Sprachen", + "Translated": "Übersetzt", + "Quantity": "Menge", + "food": "lebensmittel", + "Price": "Preis", + "Currency": "Währung", + "List of domains which can access the shared items catalog": "Liste der Domains, die auf den gemeinsam genutzten Artikelkatalog zugreifen können", + "Shares Catalog": "Aktienkatalog", + "tool": "werkzeug", + "clothes": "kleidung", + "medical": "medizinisch", + "Wanted": "Gesucht", + "Describe something wanted": "Beschreibe etwas gewünscht", + "Enter the details for your wanted item below.": "Geben Sie unten die Details zu Ihrem gewünschten Artikel ein.", + "Name of the wanted item": "Name des gesuchten Artikels", + "Description of the item wanted": "Beschreibung des gesuchten Artikels", + "Type of wanted item. eg. hat": "Art des gesuchten Artikels. z.B. Hut", + "Category of wanted item. eg. clothes": "Kategorie des gesuchten Artikels. z.B. Kleidung", + "City or location of the wanted item": "Stadt oder Ort des gesuchten Artikels", + "Maximum Price": "Höchstpreis", + "Create a new wanted item": "Erstelle einen neuen gesuchten Artikel", + "Wanted Items Search": "Gesuchte Artikel suchen", + "Website": "Webseite", + "Low Bandwidth": "Niedrige Bandbreite" } diff --git a/translations/en.json b/translations/en.json index 31d1bbf40..d65ce0eed 100644 --- a/translations/en.json +++ b/translations/en.json @@ -90,7 +90,7 @@ "View": "View", "Stop blocking": "Stop blocking", "Enter an emoji name to search for": "Enter an emoji name to search for", - "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "Enter an address, shared item, -save, !history, #hashtag, *skill or :emoji: to search for", + "Search screen text": "Enter an address, shared item, -save, 'history, #hashtag, *skill, .wanted or :emoji: to search for", "Go Back": "◀", "Moderation Information": "Moderation Information", "Suspended accounts": "Suspended accounts", @@ -107,7 +107,7 @@ "Nickname": "Nickname", "Enter Nickname": "Enter Nickname", "Password": "Password", - "Enter Password": "Enter Password", + "Enter Password": "Minimum 8 characters", "Profile for": "Profile for", "The files attached below should be no larger than 10MB in total uploaded at once.": "The files attached below should be no larger than 10MB in total uploaded at once.", "Avatar image": "Avatar image", @@ -450,5 +450,29 @@ "Export Theme": "Export Theme", "Custom post submit button text": "Custom post submit button text", "Blocked User Agents": "Blocked User Agents", - "Notify me when this account posts": "Notify me when this account posts" + "Notify me when this account posts": "Notify me when this account posts", + "Languages": "Languages", + "Translated": "Translated", + "Quantity": "Quantity", + "food": "food", + "Price": "Price", + "Currency": "Currency", + "List of domains which can access the shared items catalog": "List of domains which can access the shared items catalog", + "Shares Catalog": "Shares Catalog", + "tool": "tool", + "clothes": "clothes", + "medical": "medical", + "Wanted": "Wanted", + "Describe something wanted": "Describe something wanted", + "Enter the details for your wanted item below.": "Enter the details for your wanted item below.", + "Name of the wanted item": "Name of the wanted item", + "Description of the item wanted": "Description of the item wanted", + "Type of wanted item. eg. hat": "Type of wanted item. eg. hat", + "Category of wanted item. eg. clothes": "Category of wanted item. eg. clothes", + "City or location of the wanted item": "City or location of the wanted item", + "Maximum Price": "Maximum Price", + "Create a new wanted item": "Create a new wanted item", + "Wanted Items Search": "Wanted Items Search", + "Website": "Website", + "Low Bandwidth": "Low Bandwidth" } diff --git a/translations/es.json b/translations/es.json index ac096a01d..53c6fab3d 100644 --- a/translations/es.json +++ b/translations/es.json @@ -90,7 +90,7 @@ "View": "Ver", "Stop blocking": "Dejar de bloquear", "Enter an emoji name to search for": "Ingrese un nombre de emoji para buscar", - "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "Ingrese una dirección, elemento compartido,! Historial, #hashtag, * skill o: emoji: para buscar", + "Search screen text": "Ingrese una dirección, elemento compartido, -guardar, 'historial, #hashtag, *habilidad, .buscada o :emoji: para buscar", "Go Back": "◀", "Moderation Information": "Información de moderación", "Suspended accounts": "Cuentas suspendidas", @@ -107,7 +107,7 @@ "Nickname": "Apodo", "Enter Nickname": "Ingrese el apodo", "Password": "Contraseña", - "Enter Password": "Introducir la contraseña", + "Enter Password": "Mínimo 8 caracteres", "Profile for": "Perfil para", "The files attached below should be no larger than 10MB in total uploaded at once.": "Los archivos adjuntos a continuación no deben tener más de 10 MB en total cargados a la vez.", "Avatar image": "Imagen de avatar", @@ -450,5 +450,29 @@ "Export Theme": "Tema de exportación", "Custom post submit button text": "POST POST PERSONALIZADO Botón Texto", "Blocked User Agents": "Agentes de usuario bloqueados", - "Notify me when this account posts": "Notifíqueme cuando se publique esta cuenta" + "Notify me when this account posts": "Notifíqueme cuando se publique esta cuenta", + "Languages": "Idiomas", + "Translated": "Traducida", + "Quantity": "Cantidad", + "food": "comida", + "Price": "Precio", + "Currency": "Divisa", + "List of domains which can access the shared items catalog": "Lista de dominios que pueden acceder al catálogo de artículos compartidos", + "Shares Catalog": "Catálogo de acciones", + "tool": "herramienta", + "clothes": "ropa", + "medical": "médica", + "Wanted": "Buscada", + "Describe something wanted": "Describe algo quería", + "Enter the details for your wanted item below.": "Ingrese los detalles de su artículo deseado a continuación.", + "Name of the wanted item": "Nombre del artículo buscado", + "Description of the item wanted": "Descripción del artículo deseado", + "Type of wanted item. eg. hat": "Tipo de artículo deseado. p.ej. sombrero", + "Category of wanted item. eg. clothes": "Categoría de artículo buscado. p.ej. ropa", + "City or location of the wanted item": "Ciudad o ubicación del artículo buscado", + "Maximum Price": "Precio Máximo", + "Create a new wanted item": "Crea un nuevo artículo buscado", + "Wanted Items Search": "Búsqueda de artículos deseados", + "Website": "Sitio web", + "Low Bandwidth": "Ancho de banda bajo" } diff --git a/translations/fr.json b/translations/fr.json index 6defd9956..70788283f 100644 --- a/translations/fr.json +++ b/translations/fr.json @@ -90,7 +90,7 @@ "View": "Vue", "Stop blocking": "Arrêtez le blocage", "Enter an emoji name to search for": "Entrez un nom emoji à rechercher", - "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "Entrez une adresse, un élément partagé,! History, #hashtag, * skill ou: emoji: pour rechercher", + "Search screen text": "Entrez une adresse, un élément partagé, -enregistrer, 'l'histoire, #hashtag, *compétence, .recherchée ou :emoji: pour rechercher", "Go Back": "◀", "Moderation Information": "Informations de modération", "Suspended accounts": "Comptes suspendus", @@ -107,7 +107,7 @@ "Nickname": "Pseudo", "Enter Nickname": "Entrez le pseudo", "Password": "Mot de passe", - "Enter Password": "Entrer le mot de passe", + "Enter Password": "Minimum 8 caractères", "Profile for": "Profil pour", "The files attached below should be no larger than 10MB in total uploaded at once.": "Les fichiers joints ci-dessous ne doivent pas dépasser 10 Mo au total, téléchargés en même temps.", "Avatar image": "Image d'avatar", @@ -450,5 +450,29 @@ "Export Theme": "Thème d'exportation", "Custom post submit button text": "Texte de bouton d'envoi postal personnalisé", "Blocked User Agents": "Agents d'utilisateur bloqués", - "Notify me when this account posts": "Avertissez-moi quand ce compte publie" + "Notify me when this account posts": "Avertissez-moi quand ce compte publie", + "Languages": "Langues", + "Translated": "Traduite", + "Quantity": "Quantité", + "food": "aliments", + "Price": "Prix", + "Currency": "Devise", + "List of domains which can access the shared items catalog": "Liste des domaines pouvant accéder au catalogue d'éléments partagés", + "Shares Catalog": "Actions Catalogue", + "tool": "outil", + "clothes": "vêtements", + "medical": "médicale", + "Wanted": "Recherchée", + "Describe something wanted": "Décrire quelque chose voulu", + "Enter the details for your wanted item below.": "Entrez les détails de votre article recherché ci-dessous.", + "Name of the wanted item": "Nom de l'article recherché", + "Description of the item wanted": "Description de l'article recherché", + "Type of wanted item. eg. hat": "Type d'article recherché. par exemple. chapeau", + "Category of wanted item. eg. clothes": "Catégorie de l'article recherché. par exemple. vêtements", + "City or location of the wanted item": "Ville ou lieu de l'article recherché", + "Maximum Price": "Prix maximum", + "Create a new wanted item": "Créer un nouvel article recherché", + "Wanted Items Search": "Recherche d'objets recherchés", + "Website": "Site Internet", + "Low Bandwidth": "Bas débit" } diff --git a/translations/ga.json b/translations/ga.json index 632b57ab0..97441e317 100644 --- a/translations/ga.json +++ b/translations/ga.json @@ -90,7 +90,7 @@ "View": "Amharc", "Stop blocking": "Stop blocáil", "Enter an emoji name to search for": "Cuir isteach ainm emoji chun cuardach a dhéanamh", - "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "Iontráil seoladh, mír roinnte ,! Stair, #hashtag, * scil nó: emoji: chun cuardach a dhéanamh", + "Search screen text": "Iontráil seoladh, mír roinnte, -sábháil, 'stair, #hashtag, *scil, .theastaigh nó :emoji: chun cuardach a dhéanamh", "Go Back": "◀", "Moderation Information": "Faisnéis Modhnóireachta", "Suspended accounts": "Cuntais ar fionraí", @@ -107,7 +107,7 @@ "Nickname": "Leasainm", "Enter Nickname": "Cuir isteach an Leasainm", "Password": "Pasfhocal", - "Enter Password": "Cuir isteach an Pasfhocal", + "Enter Password": "Íosmhéid 8 gcarachtar", "Profile for": "Próifíl do", "The files attached below should be no larger than 10MB in total uploaded at once.": "Níor chóir go mbeadh uaslódáil níos mó ná 10MB ar na comhaid atá ceangailte thíos.", "Avatar image": "Avatar image", @@ -450,5 +450,29 @@ "Export Theme": "Téama Easpórtála", "Custom post submit button text": "Post saincheaptha Cuir isteach an cnaipe Téacs", "Blocked User Agents": "Gníomhairí úsáideora blocáilte", - "Notify me when this account posts": "Cuir in iúl dom nuair a phostófar an cuntas seo" + "Notify me when this account posts": "Cuir in iúl dom nuair a phostófar an cuntas seo", + "Languages": "Teangacha", + "Translated": "Aistrithe", + "Quantity": "Cainníocht", + "food": "bia", + "Price": "Praghas a chur ar", + "Currency": "Airgeadra", + "List of domains which can access the shared items catalog": "Liosta na bhfearann a fhéadann rochtain a fháil ar chatalóg na míreanna comhroinnte", + "Shares Catalog": "Scaireanna Catalóg", + "tool": "uirlis", + "clothes": "éadaí", + "medical": "scrúdú dochtúra", + "Wanted": "Theastaigh", + "Describe something wanted": "Déan cur síos ar rud éigin a theastaíonn", + "Enter the details for your wanted item below.": "Iontráil sonraí do mhír atá uait.", + "Name of the wanted item": "Ainm an earra a theastaigh", + "Description of the item wanted": "Tuairisc ar an mír a theastaigh", + "Type of wanted item. eg. hat": "Cineál earra a theastaigh. m.sh. hata", + "Category of wanted item. eg. clothes": "Catagóir an earra a theastaigh. m.sh. éadaí", + "City or location of the wanted item": "Cathair nó suíomh an earra a theastaigh", + "Maximum Price": "Uasphraghas", + "Create a new wanted item": "Cruthaigh mír nua a theastaigh", + "Wanted Items Search": "Cuardaigh Míreanna Teastaíonn", + "Website": "Suíomh gréasáin", + "Low Bandwidth": "Bandaleithead íseal" } diff --git a/translations/hi.json b/translations/hi.json index 2f3e13f0e..00e79e04b 100644 --- a/translations/hi.json +++ b/translations/hi.json @@ -90,7 +90,7 @@ "View": "राय", "Stop blocking": "रोकना बंद करो", "Enter an emoji name to search for": "खोजने के लिए एक इमोजी नाम दर्ज करें", - "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "एक पता, साझा किया गया आइटम दर्ज करें; इतिहास, # अंश, * कौशल या: इमोजी: खोजने के लिए", + "Search screen text": "खोजने के लिए एक पता, साझा किया गया आइटम, -सेव, 'इतिहास, #hashtag, *कौशल, .चाहता था or :emoji: दर्ज करें", "Go Back": "◀", "Moderation Information": "मॉडरेशन जानकारी", "Suspended accounts": "निलंबित खाते", @@ -107,7 +107,7 @@ "Nickname": "उपनाम", "Enter Nickname": "उपनाम दर्ज करें", "Password": "पारण शब्द", - "Enter Password": "पास वर्ड दर्ज करें", + "Enter Password": "न्यूनतम 8 वर्ण", "Profile for": "के लिए प्रोफाइल", "The files attached below should be no larger than 10MB in total uploaded at once.": "नीचे दी गई फाइलें कुल मिलाकर 10MB से अधिक नहीं होनी चाहिए जो एक बार में अपलोड की गई हों।", "Avatar image": "अवतार छवि", @@ -450,5 +450,29 @@ "Export Theme": "निर्यात विषय", "Custom post submit button text": "कस्टम पोस्ट सबमिट बटन टेक्स्ट", "Blocked User Agents": "अवरुद्ध उपयोगकर्ता एजेंट", - "Notify me when this account posts": "यह खाता पोस्ट होने पर मुझे सूचित करें" + "Notify me when this account posts": "यह खाता पोस्ट होने पर मुझे सूचित करें", + "Languages": "बोली", + "Translated": "अनुवाद", + "Quantity": "मात्रा", + "food": "खाना", + "Price": "कीमत", + "Currency": "मुद्रा", + "List of domains which can access the shared items catalog": "डोमेन की सूची जो साझा आइटम कैटलॉग तक पहुंच सकती है", + "Shares Catalog": "शेयर कैटलॉग", + "tool": "साधन", + "clothes": "कपड़े", + "medical": "मेडिकल", + "Wanted": "वांछित", + "Describe something wanted": "कुछ चाहते थे का वर्णन करें", + "Enter the details for your wanted item below.": "अपनी वांछित वस्तु का विवरण नीचे दर्ज करें।", + "Name of the wanted item": "वांछित वस्तु का नाम", + "Description of the item wanted": "वांछित वस्तु का विवरण", + "Type of wanted item. eg. hat": "वांछित वस्तु का प्रकार। उदाहरण के लिए टोपी", + "Category of wanted item. eg. clothes": "वांछित वस्तु की श्रेणी। उदाहरण के लिए कपड़े", + "City or location of the wanted item": "वांछित वस्तु का शहर या स्थान", + "Maximum Price": "अधिकतम मूल्य", + "Create a new wanted item": "एक नई वांछित वस्तु बनाएँ", + "Wanted Items Search": "वांटेड आइटम सर्च", + "Website": "वेबसाइट", + "Low Bandwidth": "कम बैंडविड्थ" } diff --git a/translations/it.json b/translations/it.json index 8406ccbda..64aa96724 100644 --- a/translations/it.json +++ b/translations/it.json @@ -90,7 +90,7 @@ "View": "Vista", "Stop blocking": "Smetti di bloccare", "Enter an emoji name to search for": "Inserisci un nome emoji da cercare", - "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "Inserisci un indirizzo, un oggetto condiviso,! Storia, #hashtag, * abilità o: emoji: per cercare", + "Search screen text": "Inserisci un indirizzo, un elemento condiviso, -Salva, 'storia, #hashtag, *abilità, .ricercata o :emoji: per cercare", "Go Back": "◀", "Moderation Information": "Informazioni sulla moderazione", "Suspended accounts": "Conti sospesi", @@ -107,7 +107,7 @@ "Nickname": "Soprannome", "Enter Nickname": "Inserisci il soprannome", "Password": "Parola d'ordine", - "Enter Password": "Inserire la password", + "Enter Password": "Minimo 8 caratteri", "Profile for": "Profilo per", "The files attached below should be no larger than 10MB in total uploaded at once.": "I file allegati di seguito non devono superare i 10 MB in totale caricati contemporaneamente.", "Avatar image": "Immagine avatar", @@ -450,5 +450,29 @@ "Export Theme": "Esportare tema", "Custom post submit button text": "Pulsante di invio del post personalizzato", "Blocked User Agents": "Agenti utente bloccati", - "Notify me when this account posts": "Avvisami quando questo account messaggi" + "Notify me when this account posts": "Avvisami quando questo account messaggi", + "Languages": "Le lingue", + "Translated": "Tradotto", + "Quantity": "Quantità", + "food": "cibo", + "Price": "Prezzo", + "Currency": "Moneta", + "List of domains which can access the shared items catalog": "Elenco dei domini che possono accedere al catalogo articoli condivisi", + "Shares Catalog": "Condivide il catalogo", + "tool": "attrezzo", + "clothes": "abiti", + "medical": "medica", + "Wanted": "Ricercata", + "Describe something wanted": "Descrivi qualcosa di ricercato", + "Enter the details for your wanted item below.": "Inserisci i dettagli per l'articolo desiderato di seguito.", + "Name of the wanted item": "Nome dell'articolo desiderato", + "Description of the item wanted": "Descrizione dell'articolo desiderato", + "Type of wanted item. eg. hat": "Tipo di articolo desiderato. per esempio. cappello", + "Category of wanted item. eg. clothes": "Categoria dell'oggetto ricercato. per esempio. Abiti", + "City or location of the wanted item": "Città o posizione dell'articolo desiderato", + "Maximum Price": "Prezzo massimo", + "Create a new wanted item": "Crea un nuovo oggetto ricercato", + "Wanted Items Search": "Ricerca articoli ricercati", + "Website": "Sito web", + "Low Bandwidth": "Bassa larghezza di banda" } diff --git a/translations/ja.json b/translations/ja.json index 494661a86..2aef39f71 100644 --- a/translations/ja.json +++ b/translations/ja.json @@ -90,7 +90,7 @@ "View": "見る", "Stop blocking": "ブロックを停止", "Enter an emoji name to search for": "検索する絵文字名を入力してください", - "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "検索するアドレス、共有アイテム、!history、#ハッシュタグ、* skillまたは:emoji:を入力してください", + "Search screen text": "アドレス、共有アイテム、-保存する、 '履歴、#ハッシュタグ、*スキル、.欲しかった、または:emoji:を入力して検索します", "Go Back": "◀", "Moderation Information": "モデレーション情報", "Suspended accounts": "一時停止されたアカウント", @@ -107,7 +107,7 @@ "Nickname": "ニックネーム", "Enter Nickname": "ニックネームを入力", "Password": "パスワード", - "Enter Password": "パスワードを入力する", + "Enter Password": "最小8文字", "Profile for": "のプロフィール", "The files attached below should be no larger than 10MB in total uploaded at once.": "以下に添付するファイルは、一度にアップロードされる合計で最大10MBである必要があります。", "Avatar image": "アバター画像", @@ -450,5 +450,29 @@ "Export Theme": "テーマをエクスポートします", "Custom post submit button text": "カスタムポスト送信ボタンテキスト", "Blocked User Agents": "ブロックされたユーザーエージェント", - "Notify me when this account posts": "この口座投稿を通知する" + "Notify me when this account posts": "この口座投稿を通知する", + "Languages": "言語", + "Translated": "翻訳", + "Quantity": "量", + "food": "食物", + "Price": "価格", + "Currency": "通貨", + "List of domains which can access the shared items catalog": "共有項目カタログにアクセスできるドメインのリスト", + "Shares Catalog": "カタログを共有します", + "tool": "道具", + "clothes": "服", + "medical": "医学", + "Wanted": "欲しかった", + "Describe something wanted": "必要なものを説明してください", + "Enter the details for your wanted item below.": "必要なアイテムの詳細を以下に入力してください。", + "Name of the wanted item": "欲しいアイテムの名前", + "Description of the item wanted": "欲しいアイテムの説明", + "Type of wanted item. eg. hat": "欲しいアイテムの種類。 例えば。 帽子", + "Category of wanted item. eg. clothes": "欲しいアイテムのカテゴリー。 例えば。 服", + "City or location of the wanted item": "欲しいアイテムの都市または場所", + "Maximum Price": "最高価格", + "Create a new wanted item": "新しい欲しいアイテムを作成する", + "Wanted Items Search": "欲しいアイテム検索", + "Website": "Webサイト", + "Low Bandwidth": "低帯域幅" } diff --git a/translations/ku.json b/translations/ku.json index 8d99ddd72..2d52651c1 100644 --- a/translations/ku.json +++ b/translations/ku.json @@ -90,7 +90,7 @@ "View": "Dîtinî", "Stop blocking": "Asteng bikin", "Enter an emoji name to search for": "Ji bo lêgerînê navek emoji binivîse", - "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "Navnîşanek, hêmanek parvekirî,! Dîrok, #hashtag, * jêhatî an: emoji: lêgerîn", + "Search screen text": "Navnîşanek, xala hevbeş, -rizgarkirin, 'dîrok, #hashtag, *jêhatîbûn, .xwestin an :emoji: ji bo lêgerînê", "Go Back": "◀", "Moderation Information": "Agahdariya Moderatoriyê", "Suspended accounts": "Hesabên rawestandî", @@ -107,7 +107,7 @@ "Nickname": "Nasnav", "Enter Nickname": "Navnîşan bikin", "Password": "Şîfre", - "Enter Password": "Şifreyê têke", + "Enter Password": "Kêmtirîn 8 tîpan", "Profile for": "Profîl ji bo", "The files attached below should be no larger than 10MB in total uploaded at once.": "Pelên ku li jêr hatine vegirtin divê bi tevahî di yek carekî de barkirî ji 10 MB mezintir nebin.", "Avatar image": "Wêneyê Avatar", @@ -450,5 +450,29 @@ "Export Theme": "Mijara Export", "Custom post submit button text": "Nivîsa bişkojka paşîn a paşîn", "Blocked User Agents": "Karmendên bikarhêner asteng kirin", - "Notify me when this account posts": "Dema ku ev postên hesabê min agahdar bikin" + "Notify me when this account posts": "Dema ku ev postên hesabê min agahdar bikin", + "Languages": "Ziman", + "Translated": "Wergerandin", + "Quantity": "Jimarî", + "food": "xûrek", + "Price": "Biha", + "Currency": "Diravcins", + "List of domains which can access the shared items catalog": "Navnîşa domên ku dikarin bigihîjin kataloga tiştên parvekirî", + "Shares Catalog": "Kataloga Shares", + "tool": "hacet", + "clothes": "lebas", + "medical": "pizişkî", + "Wanted": "Xwestin", + "Describe something wanted": "Tiştek xwestin diyar bikin", + "Enter the details for your wanted item below.": "Li jêr hûrguliyên tiştê ku we dixwest binivîsin.", + "Name of the wanted item": "Navê tiştê tê xwestin", + "Description of the item wanted": "Danasîna tiştê xwestî", + "Type of wanted item. eg. hat": "Tîpa tiştê xwestî. mînak. kûm", + "Category of wanted item. eg. clothes": "Kategoriya tiştê xwestî. mînak. cilan", + "City or location of the wanted item": "Bajar an cîhê tiştê xwestî", + "Maximum Price": "Maximum Price", + "Create a new wanted item": "Tiştek xwestî ya nû biafirînin", + "Wanted Items Search": "Wanted Items Search", + "Website": "Malper", + "Low Bandwidth": "Bandwidth kêm" } diff --git a/translations/oc.json b/translations/oc.json index 411216f8a..191b28fb7 100644 --- a/translations/oc.json +++ b/translations/oc.json @@ -5,7 +5,7 @@ "These are currently suspended": "Son actualament suspenduts", "Suspended accounts": "Comptes suspenduts", "Moderation Information": "Informacions de moderacion", - "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "Picatz una adreça, un element partejat, una #etiqueta, *abilitat o :emoji: a cercar", + "Search screen text": "Picatz una adreça, un element 'partejat, una #etiqueta, *abilitat o :emoji: a cercar", "Enter an emoji name to search for": "Picatz lo nom d’un emoji per lo cercar", "Information about current blocks/suspensions": "Informacion suls blocatges/suspensions en cors", "Remove a suspension for an account nickname": "Levar la suspension d’un compte", @@ -92,7 +92,7 @@ "Background image": "Imatge de fons", "Avatar image": "Imatge d’avatar", "The files attached below should be no larger than 10MB in total uploaded at once.": "Los fichièrs junts çai-jos pòdon pas èsser mai gròsses que 10 Mo al total, enviat d’un còp.", - "Enter Password": "Picatz lo senhal", + "Enter Password": "Minimum 8 characters", "Password": "Senhal", "Nickname": "Escais-nom", "About this Instance": "A prepaus d’aquesta instància", @@ -446,5 +446,29 @@ "Export Theme": "Export Theme", "Custom post submit button text": "Custom post submit button text", "Blocked User Agents": "Blocked User Agents", - "Notify me when this account posts": "Notify me when this account posts" + "Notify me when this account posts": "Notify me when this account posts", + "Languages": "Languages", + "Translated": "Translated", + "Quantity": "Quantity", + "food": "food", + "Price": "Price", + "Currency": "Currency", + "List of domains which can access the shared items catalog": "List of domains which can access the shared items catalog", + "Shares Catalog": "Shares Catalog", + "tool": "tool", + "clothes": "clothes", + "medical": "medical", + "Wanted": "Wanted", + "Describe something wanted": "Describe something wanted", + "Enter the details for your wanted item below.": "Enter the details for your wanted item below.", + "Name of the wanted item": "Name of the wanted item", + "Description of the item wanted": "Description of the item wanted", + "Type of wanted item. eg. hat": "Type of wanted item. eg. hat", + "Category of wanted item. eg. clothes": "Category of wanted item. eg. clothes", + "City or location of the wanted item": "City or location of the wanted item", + "Maximum Price": "Maximum Price", + "Create a new wanted item": "Create a new wanted item", + "Wanted Items Search": "Wanted Items Search", + "Website": "Website", + "Low Bandwidth": "Low Bandwidth" } diff --git a/translations/pt.json b/translations/pt.json index 8d05812de..4565aa6d0 100644 --- a/translations/pt.json +++ b/translations/pt.json @@ -90,7 +90,7 @@ "View": "Visão", "Stop blocking": "Pare de bloquear", "Enter an emoji name to search for": "Digite um nome emoji para procurar", - "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "Digite um endereço, item compartilhado,! History, #hashtag, * skill ou: emoji: para procurar", + "Search screen text": "Insira um endereço, item compartilhado, -salvar, 'histórico, #hashtag, * habilidade, .procurada ou :emoji: para pesquisar", "Go Back": "◀", "Moderation Information": "Informações sobre moderação", "Suspended accounts": "Contas suspensas", @@ -107,7 +107,7 @@ "Nickname": "Apelido", "Enter Nickname": "Digite o apelido", "Password": "Senha", - "Enter Password": "Digite a senha", + "Enter Password": "Mínimo de 8 caracteres", "Profile for": "Perfil para", "The files attached below should be no larger than 10MB in total uploaded at once.": "Os arquivos anexados abaixo não devem ter mais de 10 MB no total de upload de uma só vez.", "Avatar image": "Imagem do avatar", @@ -450,5 +450,29 @@ "Export Theme": "Exportar tema", "Custom post submit button text": "Texto de botão de envio de post personalizado", "Blocked User Agents": "Agentes de usuário bloqueados", - "Notify me when this account posts": "Notifique-me quando esta conta posts" + "Notify me when this account posts": "Notifique-me quando esta conta posts", + "Languages": "Línguas", + "Translated": "Traduzida", + "Quantity": "Quantidade", + "food": "comida", + "Price": "Preço", + "Currency": "Moeda", + "List of domains which can access the shared items catalog": "Lista de domínios que podem acessar o catálogo de itens compartilhados", + "Shares Catalog": "Catálogo de ações", + "tool": "ferramenta", + "clothes": "roupas", + "medical": "médica", + "Wanted": "Procurada", + "Describe something wanted": "Descreva algo queria", + "Enter the details for your wanted item below.": "Insira os detalhes do item desejado abaixo.", + "Name of the wanted item": "Nome do item desejado", + "Description of the item wanted": "Descrição do item desejado", + "Type of wanted item. eg. hat": "Tipo de item desejado. por exemplo. chapéu", + "Category of wanted item. eg. clothes": "Categoria do item desejado. por exemplo. roupas", + "City or location of the wanted item": "Cidade ou localização do item desejado", + "Maximum Price": "Preço Máximo", + "Create a new wanted item": "Crie um novo item desejado", + "Wanted Items Search": "Pesquisa de Itens Desejados", + "Website": "Local na rede Internet", + "Low Bandwidth": "Baixa largura de banda" } diff --git a/translations/ru.json b/translations/ru.json index 4663cef78..7731076b2 100644 --- a/translations/ru.json +++ b/translations/ru.json @@ -90,7 +90,7 @@ "View": "Посмотреть", "Stop blocking": "Прекратить блокировку", "Enter an emoji name to search for": "Введите имя смайлика для поиска", - "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "Введите адрес, общий элемент,! History, #hashtag, * skill или: emoji: для поиска", + "Search screen text": "Введите адрес, общий элемент, -спасти, 'история, #хэштег, *навык, .в розыске или :emoji: для поиска", "Go Back": "◀", "Moderation Information": "Модерация Информация", "Suspended accounts": "Приостановленные аккаунты", @@ -107,7 +107,7 @@ "Nickname": "кличка", "Enter Nickname": "Введите ник", "Password": "пароль", - "Enter Password": "Введите пароль", + "Enter Password": "Минимум 8 символов", "Profile for": "Профиль для", "The files attached below should be no larger than 10MB in total uploaded at once.": "Прилагаемые ниже файлы должны быть не более 10 МБ в общей сложности загружены за один раз.", "Avatar image": "Аватар изображения", @@ -450,5 +450,29 @@ "Export Theme": "Экспортная тема", "Custom post submit button text": "Пользовательский пост Отправить кнопку текста", "Blocked User Agents": "Заблокированные пользовательские агенты", - "Notify me when this account posts": "Сообщите мне, когда эта учетная запись" + "Notify me when this account posts": "Сообщите мне, когда эта учетная запись", + "Languages": "Языки", + "Translated": "Перевод", + "Quantity": "Количество", + "food": "еда", + "Price": "Цена", + "Currency": "Валюта", + "List of domains which can access the shared items catalog": "Список доменов, которые могут получить доступ к каталогу общих пунктов", + "Shares Catalog": "Акции каталог", + "tool": "орудие труда", + "clothes": "одежда", + "medical": "медицинский", + "Wanted": "В розыске", + "Describe something wanted": "Опишите что-то хотел", + "Enter the details for your wanted item below.": "Введите ниже сведения о желаемом товаре.", + "Name of the wanted item": "Название желаемого предмета", + "Description of the item wanted": "Описание разыскиваемого предмета", + "Type of wanted item. eg. hat": "Тип разыскиваемого предмета. например. шапка", + "Category of wanted item. eg. clothes": "Категория разыскиваемого предмета. например. одежда", + "City or location of the wanted item": "Город или местонахождение разыскиваемого предмета", + "Maximum Price": "Максимальная цена", + "Create a new wanted item": "Создать новый требуемый предмет", + "Wanted Items Search": "Поиск требуемых предметов", + "Website": "Интернет сайт", + "Low Bandwidth": "Низкая пропускная способность" } diff --git a/translations/sw.json b/translations/sw.json index 4f2fa1bb7..f12cc4e80 100644 --- a/translations/sw.json +++ b/translations/sw.json @@ -90,7 +90,7 @@ "View": "Tazama", "Stop blocking": "Acha kuzuia", "Enter an emoji name to search for": "Ingiza jina la emoji kutafuta", - "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "Ingiza anwani, bidhaa iliyoshirikiwa, -Save, Historia, #Hashtag, * Ujuzi au: Emoji: Ili kutafuta", + "Search screen text": "Ingiza anwani, kipengee kilichoshirikiwa, -okoa, 'historia, #hashtag, *ustadi, .nataka au :emoji: kutafuta", "Go Back": "◀", "Moderation Information": "Maelezo ya kiasi", "Suspended accounts": "Akaunti ya kusimamishwa.", @@ -107,7 +107,7 @@ "Nickname": "Jina la utani", "Enter Nickname": "Ingiza jina la utani", "Password": "Nenosiri", - "Enter Password": "Ingiza nenosiri", + "Enter Password": "Kima cha chini cha wahusika 8", "Profile for": "Profaili kwa", "The files attached below should be no larger than 10MB in total uploaded at once.": "Faili zilizounganishwa hapa chini haipaswi kuwa kubwa kuliko 10MB kwa jumla iliyopakiwa mara moja.", "Avatar image": "Avatar picha", @@ -450,5 +450,29 @@ "Export Theme": "Tuma mandhari", "Custom post submit button text": "Ujumbe wa Desturi Wasilisha Nakala ya kifungo", "Blocked User Agents": "Wakala wa watumiaji waliozuiwa", - "Notify me when this account posts": "Nijulishe wakati akaunti hii ya akaunti." + "Notify me when this account posts": "Nijulishe wakati akaunti hii ya akaunti.", + "Languages": "Lugha", + "Translated": "Ilitafsiriwa", + "Quantity": "Wingi", + "food": "chakula", + "Price": "Bei", + "Currency": "Fedha", + "List of domains which can access the shared items catalog": "Orodha ya Domains ambayo inaweza kufikia orodha ya vitu vya pamoja", + "Shares Catalog": "Inashiriki orodha", + "tool": "chombo", + "clothes": "nguo", + "medical": "matibabu", + "Wanted": "Alitaka", + "Describe something wanted": "Eleza kitu kinachotaka", + "Enter the details for your wanted item below.": "Ingiza maelezo ya kitu unachotafuta hapa chini.", + "Name of the wanted item": "Jina la kitu kinachotafutwa", + "Description of the item wanted": "Maelezo ya bidhaa inayotakiwa", + "Type of wanted item. eg. hat": "Aina ya bidhaa inayotafutwa. km. kofia", + "Category of wanted item. eg. clothes": "Jamii ya bidhaa inayotafutwa. km. nguo", + "City or location of the wanted item": "Jiji au eneo la kitu kinachotafutwa", + "Maximum Price": "Bei ya juu", + "Create a new wanted item": "Unda kipengee kipya kinachotafutwa", + "Wanted Items Search": "Utafutaji wa Vitu vinavyotafutwa", + "Website": "Tovuti", + "Low Bandwidth": "Bandwidth ya chini" } diff --git a/translations/zh.json b/translations/zh.json index eb143dc04..8d176593c 100644 --- a/translations/zh.json +++ b/translations/zh.json @@ -90,7 +90,7 @@ "View": "视图", "Stop blocking": "停止封锁", "Enter an emoji name to search for": "输入表情符号名称以进行搜索", - "Enter an address, shared item, !history, #hashtag, *skill or :emoji: to search for": "输入地址,共享项,!历史,##标签,*技能或:emoji:进行搜索", + "Search screen text": "输入地址、共享项目、-节省、'历史、#井号、*技能、.通缉 或 :表情符号: 来搜索", "Moderation Information": "审核信息", "Suspended accounts": "暂停的帐户", "These are currently suspended": "这些目前已暂停", @@ -106,7 +106,7 @@ "Nickname": "昵称", "Enter Nickname": "输入昵称", "Password": "密码", - "Enter Password": "输入密码", + "Enter Password": "至少8个字符", "Profile for": "的个人资料", "The files attached below should be no larger than 10MB in total uploaded at once.": "一次上传的文件总数不得超过10MB。", "Avatar image": "头像图片", @@ -450,5 +450,29 @@ "Export Theme": "出口主题", "Custom post submit button text": "自定义发布提交按钮文本", "Blocked User Agents": "阻止用户代理商", - "Notify me when this account posts": "此帐户帖子时通知我" + "Notify me when this account posts": "此帐户帖子时通知我", + "Languages": "语言", + "Translated": "翻译", + "Quantity": "数量", + "food": "食物", + "Price": "价钱", + "Currency": "货币", + "List of domains which can access the shared items catalog": "可以访问共享项目目录的域名列表", + "Shares Catalog": "股票目录", + "tool": "工具", + "clothes": "衣服", + "medical": "医疗的", + "Wanted": "通缉", + "Describe something wanted": "描述一些想要的东西", + "Enter the details for your wanted item below.": "在下方输入您想要的商品的详细信息。", + "Name of the wanted item": "通缉物品名称", + "Description of the item wanted": "所需物品的描述", + "Type of wanted item. eg. hat": "通缉物品的类型。 例如。 帽子", + "Category of wanted item. eg. clothes": "通缉物品类别。 例如。 衣服", + "City or location of the wanted item": "通缉物品的城市或位置", + "Maximum Price": "最高价格", + "Create a new wanted item": "创建一个新的通缉物品", + "Wanted Items Search": "通缉物品搜索", + "Website": "网站", + "Low Bandwidth": "低带宽" } diff --git a/utils.py b/utils.py index 997cf68ce..04ab3e8ba 100644 --- a/utils.py +++ b/utils.py @@ -28,6 +28,94 @@ invalidCharacters = ( ) +def localActorUrl(httpPrefix: str, nickname: str, domainFull: str) -> str: + """Returns the url for an actor on this instance + """ + return httpPrefix + '://' + domainFull + '/users/' + nickname + + +def getActorLanguagesList(actorJson: {}) -> []: + """Returns a list containing languages used by the given actor + """ + if not actorJson.get('attachment'): + return [] + for propertyValue in actorJson['attachment']: + if not propertyValue.get('name'): + continue + if not propertyValue['name'].lower().startswith('languages'): + continue + if not propertyValue.get('type'): + continue + if not propertyValue.get('value'): + continue + if propertyValue['type'] != 'PropertyValue': + continue + if isinstance(propertyValue['value'], list): + langList = propertyValue['value'] + langList.sort() + return langList + elif isinstance(propertyValue['value'], str): + langStr = propertyValue['value'] + langListTemp = [] + if ',' in langStr: + langListTemp = langStr.split(',') + elif ';' in langStr: + langListTemp = langStr.split(';') + elif '/' in langStr: + langListTemp = langStr.split('/') + elif '+' in langStr: + langListTemp = langStr.split('+') + elif ' ' in langStr: + langListTemp = langStr.split(' ') + langList = [] + for lang in langListTemp: + lang = lang.strip() + if lang not in langList: + langList.append(lang) + langList.sort() + return langList + return [] + + +def getContentFromPost(postJsonObject: {}, systemLanguage: str, + languagesUnderstood: []) -> str: + """Returns the content from the post in the given language + including searching for a matching entry within contentMap + """ + thisPostJson = postJsonObject + if hasObjectDict(postJsonObject): + thisPostJson = postJsonObject['object'] + if not thisPostJson.get('content'): + return '' + content = '' + if thisPostJson.get('contentMap'): + if isinstance(thisPostJson['contentMap'], dict): + if thisPostJson['contentMap'].get(systemLanguage): + if isinstance(thisPostJson['contentMap'][systemLanguage], str): + return thisPostJson['contentMap'][systemLanguage] + else: + # is there a contentMap entry for one of + # the understood languages? + for lang in languagesUnderstood: + if thisPostJson['contentMap'].get(lang): + return thisPostJson['contentMap'][lang] + else: + if isinstance(thisPostJson['content'], str): + content = thisPostJson['content'] + return content + + +def getBaseContentFromPost(postJsonObject: {}, systemLanguage: str) -> str: + """Returns the content from the post in the given language + """ + thisPostJson = postJsonObject + if hasObjectDict(postJsonObject): + thisPostJson = postJsonObject['object'] + if not thisPostJson.get('content'): + return '' + return thisPostJson['content'] + + def acctDir(baseDir: str, nickname: str, domain: str) -> str: return baseDir + '/accounts/' + nickname + '@' + domain @@ -96,9 +184,9 @@ def getLockedAccount(actorJson: {}) -> bool: def hasUsersPath(pathStr: str) -> bool: """Whether there is a /users/ path (or equivalent) in the given string """ - usersList = ('users', 'accounts', 'channel', 'profile', 'u') + usersList = getUserPaths() for usersStr in usersList: - if '/' + usersStr + '/' in pathStr: + if usersStr in pathStr: return True if '://' in pathStr: domain = pathStr.split('://')[1] @@ -234,6 +322,18 @@ def isArtist(baseDir: str, nickname: str) -> bool: return False +def getVideoExtensions() -> []: + """Returns a list of the possible video file extensions + """ + return ('mp4', 'webm', 'ogv') + + +def getAudioExtensions() -> []: + """Returns a list of the possible audio file extensions + """ + return ('mp3', 'ogg', 'flac') + + def getImageExtensions() -> []: """Returns a list of the possible image file extensions """ @@ -274,18 +374,6 @@ def getImageExtensionFromMimeType(contentType: str) -> str: return 'png' -def getVideoExtensions() -> []: - """Returns a list of the possible video file extensions - """ - return ('mp4', 'webm', 'ogv') - - -def getAudioExtensions() -> []: - """Returns a list of the possible audio file extensions - """ - return ('mp3', 'ogg') - - def getMediaExtensions() -> []: """Returns a list of the possible media file extensions """ @@ -789,6 +877,8 @@ def _genderFromString(translate: {}, text: str) -> str: """Given some text, does it contain a gender description? """ gender = None + if not text: + return None textOrig = text text = text.lower() if translate['He/Him'].lower() in text or \ @@ -909,8 +999,16 @@ def getNicknameFromActor(actor: str) -> str: def getUserPaths() -> []: """Returns possible user paths + e.g. /users/nickname, /channel/nickname """ - return ('/users/', '/profile/', '/accounts/', '/channel/', '/u/') + return ('/users/', '/profile/', '/accounts/', '/channel/', '/u/', '/c/') + + +def getGroupPaths() -> []: + """Returns possible group paths + e.g. https://lemmy/c/groupname + """ + return ['/c/'] def getDomainFromActor(actor: str) -> (str, int): @@ -978,7 +1076,8 @@ def _setDefaultPetName(baseDir: str, nickname: str, domain: str, def followPerson(baseDir: str, nickname: str, domain: str, followNickname: str, followDomain: str, federationList: [], debug: bool, - followFile='following.txt') -> bool: + groupAccount: bool, + followFile: str = 'following.txt') -> bool: """Adds a person to the follow list """ followDomainStrLower = followDomain.lower().replace('\n', '') @@ -1007,6 +1106,9 @@ def followPerson(baseDir: str, nickname: str, domain: str, else: handleToFollow = followNickname + '@' + followDomain + if groupAccount: + handleToFollow = '!' + handleToFollow + # was this person previously unfollowed? unfollowedFilename = baseDir + '/accounts/' + handle + '/unfollowed.txt' if os.path.isfile(unfollowedFilename): @@ -1024,6 +1126,8 @@ def followPerson(baseDir: str, nickname: str, domain: str, if not os.path.isdir(baseDir + '/accounts'): os.mkdir(baseDir + '/accounts') handleToFollow = followNickname + '@' + followDomain + if groupAccount: + handleToFollow = '!' + handleToFollow filename = baseDir + '/accounts/' + handle + '/' + followFile if os.path.isfile(filename): if handleToFollow in open(filename).read(): @@ -1389,6 +1493,38 @@ def _deleteHashtagsOnPost(baseDir: str, postJsonObject: {}) -> None: f.write(newlines) +def _deleteConversationPost(baseDir: str, nickname: str, domain: str, + postJsonObject: {}) -> None: + """Deletes a post from a conversation + """ + if not hasObjectDict(postJsonObject): + return False + if not postJsonObject['object'].get('conversation'): + return False + if not postJsonObject['object'].get('id'): + return False + conversationDir = acctDir(baseDir, nickname, domain) + '/conversation' + conversationId = postJsonObject['object']['conversation'] + conversationId = conversationId.replace('/', '#') + postId = postJsonObject['object']['id'] + conversationFilename = conversationDir + '/' + conversationId + if not os.path.isfile(conversationFilename): + return False + conversationStr = '' + with open(conversationFilename, 'r') as fp: + conversationStr = fp.read() + if postId + '\n' not in conversationStr: + return False + conversationStr = conversationStr.replace(postId + '\n', '') + if conversationStr: + with open(conversationFilename, 'w+') as fp: + fp.write(conversationStr) + else: + if os.path.isfile(conversationFilename + '.muted'): + os.remove(conversationFilename + '.muted') + os.remove(conversationFilename) + + def deletePost(baseDir: str, httpPrefix: str, nickname: str, domain: str, postFilename: str, debug: bool, recentPostsCache: {}) -> None: @@ -1416,6 +1552,9 @@ def deletePost(baseDir: str, httpPrefix: str, # remove from recent posts cache in memory removePostFromCache(postJsonObject, recentPostsCache) + # remove from conversation index + _deleteConversationPost(baseDir, nickname, domain, postJsonObject) + # remove any attachment _removeAttachment(baseDir, httpPrefix, domain, postJsonObject) @@ -1501,27 +1640,45 @@ def isValidLanguage(text: str) -> bool: return False +def _getReservedWords() -> str: + return ('inbox', 'dm', 'outbox', 'following', + 'public', 'followers', 'category', + 'channel', 'calendar', + 'tlreplies', 'tlmedia', 'tlblogs', + 'tlblogs', 'tlfeatures', + 'moderation', 'moderationaction', + 'activity', 'undo', 'pinned', + 'reply', 'replies', 'question', 'like', + 'likes', 'users', 'statuses', 'tags', + 'accounts', 'headers', + 'channels', 'profile', 'u', 'c', + 'updates', 'repeat', 'announce', + 'shares', 'fonts', 'icons', 'avatars', + 'welcome', 'helpimages', + 'bookmark', 'bookmarks', 'tlbookmarks', + 'ignores', 'linksmobile', 'newswiremobile', + 'minimal', 'search', 'eventdelete', + 'searchemoji', 'catalog', 'conversationId', + 'mention', 'http', 'https') + + +def getNicknameValidationPattern() -> str: + """Returns a html text input validation pattern for nickname + """ + reservedNames = _getReservedWords() + pattern = '' + for word in reservedNames: + if pattern: + pattern += '(?!.*\\b' + word + '\\b)' + else: + pattern = '^(?!.*\\b' + word + '\\b)' + return pattern + '.*${1,30}' + + def _isReservedName(nickname: str) -> bool: """Is the given nickname reserved for some special function? """ - reservedNames = ('inbox', 'dm', 'outbox', 'following', - 'public', 'followers', 'category', - 'channel', 'calendar', - 'tlreplies', 'tlmedia', 'tlblogs', - 'tlblogs', 'tlfeatures', - 'moderation', 'moderationaction', - 'activity', 'undo', 'pinned', - 'reply', 'replies', 'question', 'like', - 'likes', 'users', 'statuses', 'tags', - 'accounts', 'headers', - 'channels', 'profile', 'u', - 'updates', 'repeat', 'announce', - 'shares', 'fonts', 'icons', 'avatars', - 'welcome', 'helpimages', - 'bookmark', 'bookmarks', 'tlbookmarks', - 'ignores', 'linksmobile', 'newswiremobile', - 'minimal', 'search', 'eventdelete', - 'searchemoji') + reservedNames = _getReservedWords() if nickname in reservedNames: return True return False @@ -1530,9 +1687,13 @@ def _isReservedName(nickname: str) -> bool: def validNickname(domain: str, nickname: str) -> bool: """Is the given nickname valid? """ + if len(nickname) == 0: + return False + if len(nickname) > 30: + return False if not isValidLanguage(nickname): return False - forbiddenChars = ('.', ' ', '/', '?', ':', ';', '@', '#') + forbiddenChars = ('.', ' ', '/', '?', ':', ';', '@', '#', '!') for c in forbiddenChars: if c in nickname: return False @@ -1715,41 +1876,6 @@ def getCSS(baseDir: str, cssFilename: str, cssCache: {}) -> str: return None -def isEventPost(messageJson: {}) -> bool: - """Is the given post a mobilizon-type event activity? - See https://framagit.org/framasoft/mobilizon/-/blob/ - master/lib/federation/activity_stream/converter/event.ex - """ - if not messageJson.get('id'): - return False - if not messageJson.get('actor'): - return False - if not hasObjectDict(messageJson): - return False - if not messageJson['object'].get('type'): - return False - if messageJson['object']['type'] != 'Event': - return False - print('Event arriving') - if not messageJson['object'].get('startTime'): - print('No event start time') - return False - if not messageJson['object'].get('actor'): - print('No event actor') - return False - if not messageJson['object'].get('content'): - print('No event content') - return False - if not messageJson['object'].get('name'): - print('No event name') - return False - if not messageJson['object'].get('uuid'): - print('No event UUID') - return False - print('Event detected') - return True - - def isBlogPost(postJsonObject: {}) -> bool: """Is the given post a blog post? """ @@ -2151,6 +2277,7 @@ def mediaFileMimeType(filename: str) -> str: 'avif': 'image/avif', 'mp3': 'audio/mpeg', 'ogg': 'audio/ogg', + 'flac': 'audio/flac', 'mp4': 'video/mp4', 'ogv': 'video/ogv' } @@ -2578,3 +2705,205 @@ def validUrlPrefix(url: str) -> bool: if url.startswith(pre): return True return False + + +def removeLineEndings(text: str) -> str: + """Removes any newline from the end of a string + """ + text = text.replace('\n', '') + text = text.replace('\r', '') + return text.strip() + + +def validPassword(password: str) -> bool: + """Returns true if the given password is valid + """ + if len(password) < 8: + return False + return True + + +def isfloat(value): + try: + float(value) + return True + except ValueError: + return False + + +def dateStringToSeconds(dateStr: str) -> int: + """Converts a date string (eg "published") into seconds since epoch + """ + try: + expiryTime = \ + datetime.datetime.strptime(dateStr, '%Y-%m-%dT%H:%M:%SZ') + except BaseException: + return None + return int(datetime.datetime.timestamp(expiryTime)) + + +def dateSecondsToString(dateSec: int) -> str: + """Converts a date in seconds since epoch to a string + """ + thisDate = datetime.datetime.fromtimestamp(dateSec) + return thisDate.strftime("%Y-%m-%dT%H:%M:%SZ") + + +def hasGroupType(baseDir: str, actor: str, personCache: {}, + debug: bool = False) -> bool: + """Does the given actor url have a group type? + """ + # does the actor path clearly indicate that this is a group? + # eg. https://lemmy/c/groupname + groupPaths = getGroupPaths() + for grpPath in groupPaths: + if grpPath in actor: + if debug: + print('grpPath ' + grpPath + ' in ' + actor) + return True + # is there a cached actor which can be examined for Group type? + return isGroupActor(baseDir, actor, personCache, debug) + + +def isGroupActor(baseDir: str, actor: str, personCache: {}, + debug: bool = False) -> bool: + """Is the given actor a group? + """ + if personCache: + if personCache.get(actor): + if personCache[actor].get('actor'): + if personCache[actor]['actor'].get('type'): + if personCache[actor]['actor']['type'] == 'Group': + if debug: + print('Cached actor ' + actor + ' has Group type') + return True + return False + if debug: + print('Actor ' + actor + ' not in cache') + cachedActorFilename = \ + baseDir + '/cache/actors/' + (actor.replace('/', '#')) + '.json' + if not os.path.isfile(cachedActorFilename): + if debug: + print('Cached actor file not found ' + cachedActorFilename) + return False + if '"type": "Group"' in open(cachedActorFilename).read(): + if debug: + print('Group type found in ' + cachedActorFilename) + return True + return False + + +def isGroupAccount(baseDir: str, nickname: str, domain: str) -> bool: + """Returns true if the given account is a group + """ + accountFilename = acctDir(baseDir, nickname, domain) + '.json' + if not os.path.isfile(accountFilename): + return False + if '"type": "Group"' in open(accountFilename).read(): + return True + return False + + +def getCurrencies() -> {}: + """Returns a dictionary of currencies + """ + return { + "CA$": "CAD", + "J$": "JMD", + "£": "GBP", + "€": "EUR", + "؋": "AFN", + "ƒ": "AWG", + "₼": "AZN", + "Br": "BYN", + "BZ$": "BZD", + "$b": "BOB", + "KM": "BAM", + "P": "BWP", + "лв": "BGN", + "R$": "BRL", + "៛": "KHR", + "$U": "UYU", + "RD$": "DOP", + "$": "USD", + "₡": "CRC", + "kn": "HRK", + "₱": "CUP", + "Kč": "CZK", + "kr": "NOK", + "¢": "GHS", + "Q": "GTQ", + "L": "HNL", + "Ft": "HUF", + "Rp": "IDR", + "₹": "INR", + "﷼": "IRR", + "₪": "ILS", + "¥": "JPY", + "₩": "KRW", + "₭": "LAK", + "ден": "MKD", + "RM": "MYR", + "₨": "MUR", + "₮": "MNT", + "MT": "MZN", + "C$": "NIO", + "₦": "NGN", + "Gs": "PYG", + "zł": "PLN", + "lei": "RON", + "₽": "RUB", + "Дин": "RSD", + "S": "SOS", + "R": "ZAR", + "CHF": "CHF", + "NT$": "TWD", + "฿": "THB", + "TT$": "TTD", + "₴": "UAH", + "Bs": "VEF", + "₫": "VND", + "Z$": "ZQD" + } + + +def getSupportedLanguages(baseDir: str) -> []: + """Returns a list of supported languages + """ + translationsDir = baseDir + '/translations' + languagesStr = [] + for subdir, dirs, files in os.walk(translationsDir): + for f in files: + if not f.endswith('.json'): + continue + lang = f.split('.')[0] + if len(lang) == 2: + languagesStr.append(lang) + break + return languagesStr + + +def getCategoryTypes(baseDir: str) -> []: + """Returns the list of ontologies + """ + ontologyDir = baseDir + '/ontology' + categories = [] + for subdir, dirs, files in os.walk(ontologyDir): + for f in files: + if not f.endswith('.json'): + continue + if '#' in f or '~' in f: + continue + if f.startswith('custom'): + continue + ontologyFilename = f.split('.')[0] + if 'Types' in ontologyFilename: + categories.append(ontologyFilename.replace('Types', '')) + break + return categories + + +def getSharesFilesList() -> []: + """Returns the possible shares files + """ + return ('shares', 'wanted') diff --git a/webapp_calendar.py b/webapp_calendar.py index 10e480fc1..81e4e65d8 100644 --- a/webapp_calendar.py +++ b/webapp_calendar.py @@ -21,6 +21,7 @@ from utils import weekDayOfMonthStart from utils import getAltPath from utils import removeDomainPort from utils import acctDir +from utils import localActorUrl from happening import getTodaysEvents from happening import getCalendarEvents from webapp_utils import htmlHeaderWithExternalStyle @@ -37,7 +38,7 @@ def htmlCalendarDeleteConfirm(cssCache: {}, translate: {}, baseDir: str, """Shows a screen asking to confirm the deletion of a calendar event """ nickname = getNicknameFromActor(path) - actor = httpPrefix + '://' + domainFull + '/users/' + nickname + actor = localActorUrl(httpPrefix, nickname, domainFull) domain, port = getDomainFromActor(actor) messageId = actor + '/statuses/' + postId diff --git a/webapp_column_left.py b/webapp_column_left.py index 034153184..33cd04a51 100644 --- a/webapp_column_left.py +++ b/webapp_column_left.py @@ -12,6 +12,7 @@ from utils import getConfigParam from utils import getNicknameFromActor from utils import isEditor from utils import removeDomainPort +from utils import localActorUrl from webapp_utils import sharesTimelineJson from webapp_utils import htmlPostSeparator from webapp_utils import getLeftImageFile @@ -29,18 +30,21 @@ def _linksExist(baseDir: str) -> bool: def _getLeftColumnShares(baseDir: str, - httpPrefix: str, domainFull: str, + httpPrefix: str, domain: str, domainFull: str, nickname: str, maxSharesInLeftColumn: int, - translate: {}) -> []: + translate: {}, + sharedItemsFederatedDomains: []) -> []: """get any shares and turn them into the left column links format """ pageNumber = 1 - actor = httpPrefix + '://' + domainFull + '/users/' + nickname + actor = localActorUrl(httpPrefix, nickname, domainFull) + # NOTE: this could potentially be slow if the number of federated + # shared items is large sharesJson, lastPage = \ - sharesTimelineJson(actor, pageNumber, - maxSharesInLeftColumn, - baseDir, maxSharesInLeftColumn) + sharesTimelineJson(actor, pageNumber, maxSharesInLeftColumn, + baseDir, domain, nickname, maxSharesInLeftColumn, + sharedItemsFederatedDomains, 'shares') if not sharesJson: return [] @@ -50,9 +54,9 @@ def _getLeftColumnShares(baseDir: str, sharedesc = item['displayName'] if '<' in sharedesc or '?' in sharedesc: continue - contactActor = item['actor'] - shareLink = actor + '?replydm=sharedesc:' + \ - sharedesc.replace(' ', '_') + '?mention=' + contactActor + shareId = item['shareId'] + # selecting this link calls htmlShowShare + shareLink = actor + '?showshare=' + shareId linksList.append(sharedesc + ' ' + shareLink) ctr += 1 if ctr >= maxSharesInLeftColumn: @@ -63,13 +67,52 @@ def _getLeftColumnShares(baseDir: str, return linksList +def _getLeftColumnWanted(baseDir: str, + httpPrefix: str, domain: str, domainFull: str, + nickname: str, + maxSharesInLeftColumn: int, + translate: {}, + sharedItemsFederatedDomains: []) -> []: + """get any wanted items and turn them into the left column links format + """ + pageNumber = 1 + actor = localActorUrl(httpPrefix, nickname, domainFull) + # NOTE: this could potentially be slow if the number of federated + # wanted items is large + sharesJson, lastPage = \ + sharesTimelineJson(actor, pageNumber, maxSharesInLeftColumn, + baseDir, domain, nickname, maxSharesInLeftColumn, + sharedItemsFederatedDomains, 'wanted') + if not sharesJson: + return [] + + linksList = [] + ctr = 0 + for published, item in sharesJson.items(): + sharedesc = item['displayName'] + if '<' in sharedesc or '?' in sharedesc: + continue + shareId = item['shareId'] + # selecting this link calls htmlShowShare + shareLink = actor + '?showwanted=' + shareId + linksList.append(sharedesc + ' ' + shareLink) + ctr += 1 + if ctr >= maxSharesInLeftColumn: + break + + if linksList: + linksList = ['* ' + translate['Wanted']] + linksList + return linksList + + def getLeftColumnContent(baseDir: str, nickname: str, domainFull: str, httpPrefix: str, translate: {}, editor: bool, showBackButton: bool, timelinePath: str, rssIconAtTop: bool, showHeaderImage: bool, frontPage: bool, theme: str, - accessKeys: {}) -> str: + accessKeys: {}, + sharedItemsFederatedDomains: []) -> str: """Returns html content for the left column """ htmlStr = '' @@ -158,11 +201,21 @@ def getLeftColumnContent(baseDir: str, nickname: str, domainFull: str, maxSharesInLeftColumn = 3 sharesList = \ _getLeftColumnShares(baseDir, - httpPrefix, domainFull, nickname, - maxSharesInLeftColumn, translate) + httpPrefix, domain, domainFull, nickname, + maxSharesInLeftColumn, translate, + sharedItemsFederatedDomains) if linksList and sharesList: linksList = sharesList + linksList + wantedList = \ + _getLeftColumnWanted(baseDir, + httpPrefix, domain, domainFull, nickname, + maxSharesInLeftColumn, translate, + sharedItemsFederatedDomains) + if linksList and wantedList: + linksList = wantedList + linksList + + newTabStr = ' target="_blank" rel="nofollow noopener noreferrer"' if linksList: htmlStr += '