Merge discourse-narrative-bot into core plugins.

This commit is contained in:
Guo Xiang Tan 2017-05-24 13:50:20 +08:00
parent 796a2967af
commit 7f0561b621
52 changed files with 7298 additions and 0 deletions

1
.gitignore vendored
View File

@ -50,6 +50,7 @@ log/
!/plugins/poll/
!/plugins/discourse-details/
!/plugins/discourse-nginx-performance-report
!/plugins/discourse-narrative-bot
/plugins/*/auto_generated/
/spec/fixtures/plugins/my_plugin/auto_generated

View File

@ -0,0 +1,39 @@
import { withPluginApi } from 'discourse/lib/plugin-api';
function initialize(api) {
const messageBus = api.container.lookup('message-bus:main');
const currentUser = api.getCurrentUser();
const appEvents = api.container.lookup('app-events:main');
const SiteHeaderComponent = api.container.lookupFactory('component:site-header');
SiteHeaderComponent.reopen({
didInsertElement() {
this._super();
this.dispatch('header:search-context-trigger', 'header');
}
});
api.attachWidgetAction('header', 'headerSearchContextTrigger', function() {
if (this.site.mobileView) {
this.state.skipSearchContext = false;
} else {
this.state.contextEnabled = true;
this.state.searchContextType = 'topic';
}
});
if (messageBus && currentUser) {
messageBus.subscribe(`/new_user_narrative/tutorial_search`, () => {
appEvents.trigger('header:search-context-trigger');
});
}
}
export default {
name: "new-user-narratve",
initialize(container) {
const siteSettings = container.lookup('site-settings:main');
if (siteSettings.discourse_narrative_bot_enabled) withPluginApi('0.5', initialize);
}
};

View File

@ -0,0 +1,6 @@
de:
js:
discourse_narrative_bot:
welcome_post_type:
new_user_track: "Starte bei allen neuen Benutzern das „Tutorial für neue Benutzer“"
welcome_message: "Sende allen neuen Benutzern eine Willkommensnachricht mit einer Kurzanleitung"

View File

@ -0,0 +1,6 @@
en:
js:
discourse_narrative_bot:
welcome_post_type:
new_user_track: "Start the new user tutorial for all new users"
welcome_message: "Send all new users a welcome message with a quick start guide"

View File

@ -0,0 +1,6 @@
es:
js:
discourse_narrative_bot:
welcome_post_type:
new_user_track: "Comenzar el tutorial de nuevos usuarios para todos los nuevos usuarios"
welcome_message: "Enviar a todos los nuevos usuarios un mensaje de bienvenida con la guía de comienzo rápida"

View File

@ -0,0 +1,6 @@
fi:
js:
discourse_narrative_bot:
welcome_post_type:
new_user_track: "Aloita opastuskurssi kaikkien uusien käyttäjien kanssa"
welcome_message: "Lähetä kaikille uusille käyttäjille tervetuloviesti, jossa on pikaopas palstan käyttöön"

View File

@ -0,0 +1,422 @@
de:
site_settings:
discourse_narrative_bot_enabled: 'Discourse Narrative Bot aktivieren'
disable_discourse_narrative_bot_welcome_post: "Die vom Discourse Narrative Bot gesendete Willkommensnachricht deaktivieren"
discourse_narrative_bot_ignored_usernames: "Benutzernamen, die vom Discourse Narrative Bot ignoriert werden sollen"
discourse_narrative_bot_disable_public_replies: "Öffentliche Antworten von Discourse Narrative Bot deaktivieren"
discourse_narrative_bot_welcome_post_type: "Die Art der Willkommensnachricht, die vom Discourse Narrative Bot versendet werden soll."
badges:
certified:
name: Zertifiziert
description: "Hat das Tutorial für neue Benutzer abgeschlossen"
long_description: |
Das Abzeichen wird verliehen, wenn das interaktive Tutorial für neue Benutzer erfolgreich abgeschlossen wurde. Du hast die Grundlagen für Diskussionen erlernt und bist nun zertifiziert.
licensed:
name: Lizenziert
description: "Hat das Tutorial für fortgeschrittene Benutzer abgeschlossen"
long_description: |
Das Abzeichen wird verliehen, wenn das interaktive Tutorial für fortgeschrittene Benutzer erfolgreich abgeschlossen wurde. Du beherrscht die fortgeschrittenen Werkzeuge für Diskussionen erlernt und besitzt nun die Lizenz zum Diskutieren.
discourse_narrative_bot:
bio: "Hallo! Ich bin keine reale Person. Ich bin ein Bot, der dir etwas über diese Website beibringen kann. Schick mir eine Nachricht oder erwähne irgendwo **`@%{discobot_username}`**, um mit mir zu interagieren."
timeout:
message: |-
Hallo @%{username}! Ich wollte mich nur wieder einmal melden, weil ich schon länger nichts von dir gehört habe.
- Um fortzusetzen, antworte mir jederzeit.
- Wenn du diesen Schritt überspringen möchtest, antworte mit `%{skip_trigger}`.
- Um von vorne zu beginnen, antworte mit `%{reset_trigger}`.
Wenn du mir lieber nicht schreiben möchtest, ist das auch okay. Ich bin ein Roboter. Du wirst meine Gefühle nicht verletzen. :sob:
dice:
trigger: "würfeln"
invalid: |-
Es tut mir leid, aber das Rollen dieser Kombination von Würfeln ist mathematisch unmöglich. :confounded:
not_enough_dice: |-
Ich habe nur %{num_of_dice} Würfel. [Beschämend](http://www.therobotsvoice.com/2009/04/the_10_most_shameful_rpg_dice.php), ich weiß!
out_of_range: |-
Wusstest du, dass ein fairer Würfel aus [maximal 120 Seiten](http://www.spiegel.de/wissenschaft/mensch/d120-groesster-wuerfel-der-welt-hat-120-seiten-a-1099170.html) besteht?
results: |-
> :game_die: %{results}
quote:
trigger: "Zitat"
'1':
quote: "In der Mitte von Schwierigkeiten liegen die Möglichkeiten."
author: "Albert Einstein"
'2':
quote: "Sei du selbst die Veränderung, die du dir wünschst für diese Welt."
author: "Mahatma Gandhi"
'3':
quote: "Weine nicht, weil es vorbei ist. Lächle, weil es passiert ist."
author: "Dr. Seuss"
'4':
quote: "Gut ist man nur bedient, wenn man sich selbst bedient."
author: "Charles-Guillaume Étienne"
'5':
quote: "Glaube daran, dass du etwas kannst und du hast es schon halb geschafft."
author: "Theodore Roosevelt"
'6':
quote: "Das Leben ist wie eine Schachtel Pralinen. Man weiß nie, was man kriegt."
author: "Forrest Gump"
'7':
quote: "Das ist ein kleiner Schritt für einen Menschen, ein riesiger Sprung für die Menschheit."
author: "Neil Armstrong"
'8':
quote: "Tue jeden Tag etwas, wovor du Angst hast."
author: "Eleanor Roosevelt"
'9':
quote: "Fehler sind immer zu verzeihen, wenn man den Mut hat, diese auch zuzugeben."
author: "Bruce Lee"
'10':
quote: "Was der Verstand eines Menschen begreifen und glauben kann, kann er erreichen."
author: "Napoleon Hill"
results: |-
> :left_speech_bubble: _%{quote}_ — %{author}
magic_8_ball:
trigger: 'Vorhersage'
answers:
'1': "Es ist sicher"
'2': "Es ist entschieden so"
'3': "Ohne einen Zweifel"
'4': "Definitiv ja"
'5': "Darauf kannst du dich verlassen"
'6': "Wie ich es sehe, ja"
'7': "Höchstwahrscheinlich"
'8': "Gute Aussichten"
'9': "Ja"
'10': "Zeichen deuten auf ja"
'11': "Antwort unklar, versuch's nochmal"
'12': "Frag später nochmal"
'13': "Ich sags dir jetzt lieber nicht"
'14': "Kann es jetzt nicht vorhersagen"
'15': "Konzentriere dich und frag nochmal"
'16': "Warts erst mal ab."
'17': "Meine Antwort ist nein"
'18': "Meine Quellen sagen nein"
'19': "Nicht so gute Aussichten"
'20': "Sehr zweifelhaft"
result: |-
> :crystal_ball: %{result}
track_selector:
reset_trigger: 'starte'
skip_trigger: 'überspringen'
help_trigger: 'Hilfe anzeigen'
random_mention:
reply: |-
Hallo! Um herauszufinden, was ich kann, schreibe `@%{discobot_username} %{help_trigger}`.
tracks: |-
Ich weiß derzeit, wie man die folgenden Dinge macht:
`@%{discobot_username} %{reset_trigger} %{default_track}
> Startet eine der folgenden interaktiven Tutorials: %{tracks}.
bot_actions: |-
`@%{discobot_username} %{dice_trigger} 2d6`
> :game_die: 3, 6
`@%{discobot_username} %{quote_trigger}`
> :left_speech_bubble: _Wie angenehm ist es doch, freundlich zu sein! Ein gutes Wort entschlüpft wie ein wohliger Seufzer._ — Bertolt Brecht
`@%{discobot_username} %{magic_8_ball_trigger}`
> :crystal_ball: Darauf kannst du dich verlassen
do_not_understand:
first_response: |-
Danke für die Antwort!
Weil ich ein schlecht programmierter Bot bin, habe ich das leider nicht ganz verstanden. :frowning:
track_response: Du kannst es nochmal versuchen, oder, wenn du diesen Schritt überspringen möchtest, antworte mit `%{skip_trigger}`. Falls du von vorne beginnen möchtest, antworte mit `%{reset_trigger}`.
second_response: |-
Es tut mir leid, aber ich verstehe es noch immer nicht. :anguished:
Ich bin nur ein Bot. Schau dir [unsere Kontaktinformationen](/about) an, falls du dich mit einer echten Person unterhalten möchtest.
In der Zwischenzeit lass ich dich erst mal in Ruhe.
new_user_narrative:
reset_trigger: "neuer Benutzer"
cert_title: "In Anerkennung deines erfolgreichen Abschlusses eines Tutorials für neue Benutzer"
hello:
title: ":robot: Grüß dich!"
message: |-
Willkommen bei %{title}. Danke, dass du beigetreten bist.
- Ich bin nur ein Roboter, aber [unser freundliches Team](/about) ist auch da um zu helfen, wenn du eine Person erreichen möchtest.
- Aus Sicherheitsgründen begrenzen wir vorübergehend, was neue Benutzer tun können. Du wirst neue Fähigkeiten (und [Abzeichen](/badges)) erhalten, während wir uns kennenlernen.
- Wir glauben an [zivilisiertes Community-Verhalten](/guidelines) zu allen Zeiten.
onebox:
instructions: |-
Als nächstes: Kannst du einen dieser Links mit mir teilen? Antworte mit **einem Link auf einer eigenen Zeile**, und er wird automatisch in eine hübsche, kurze Inhaltsangabe erweitert.
Um einen Link zu kopieren, tippe und halte auf mobilen Geräten, oder klicke rechts mit deiner Maus:
- https://de.wikipedia.org/wiki/Antiwitz
- https://de.wikipedia.org/wiki/Tetraphobie
- https://de.wikipedia.org/wiki/Beghilos
reply: |-
Cool! Dies wird mit den meisten <img src="/images/font-awesome-link.png" width="16" height="16"> Links funktionieren. Denk immer daran, dass der Link _ganz allein_ auf einer Zeile stehen muss; ohne etwas davor oder dahinter.
not_found: |-
Entschuldige bitte, ich konnte keinen Link in deiner Antwort finden! :cry:
Kannst du versuchen, den folgenden Link auf einer eigenen Zeile in deine nächste Antwort einzufügen?
- https://de.wikipedia.org/wiki/Hauskatze
images:
instructions: |-
Hier ist ein Bild von einem Einhorn:
<img src="/images/unicorn.jpg" width="520" height="381">
Wenn du es magst (und wer tut das nicht!), leg los und drücke die Like-Schaltfläche :heart: unter diesem Beitrag, um es mich wissen zu lassen.
Kannst du **mit einem Bild antworten?** Egal welches Bild! Ziehe das Bild in den Browser, drücke die Upload-Schaltfläche, oder kopiere es in die Zwischenablage und füge es ein.
reply: |-
Hübsches Bild ich habe die Like-Schaltfläche :heart: gedrückt, um dich wissen zu lassen, wie sehr es mir gefällt :heart_eyes:
like_not_found: |-
Hast du vergessen meinen [Beitrag](%{url}) mit einem Like :heart: zu markieren? :crying_cat_face:
not_found: |-
Sieht so aus, als hättest du kein Bild hochgeladen. Deshalb habe ich ein Bild ausgesucht, von dem ich _sicher_ bin, dass es dir gefällt.
`%{image_url}`
Versuche dieses als nächstes hochzuladen, oder füge den Link auf einer eigenen Zeile ein!
formatting:
instructions: |-
Kannst du einige Wörter in deiner Antwort **fett** oder _kursiv_ markieren?
- schreibe `**fett**` oder `_kursiv_`
- oder, drücke die <kbd><b>F</b></kbd>- oder <kbd><i>K</i></kbd>-Schaltflächen im Editor
reply: |-
Großartige Leistung! HTML und BBCode funktionieren auch zur Formatierung um mehr zu erfahren, [probiere dieses Tutorial aus](http://commonmark.org/help) :nerd:
not_found: |-
Ach, ich habe keine Formatierung in deiner Antwort gefunden. :pencil2:
Kannst du es nochmal versuchen? Verwende die <kbd><b>F</b></kbd>- (Fett) oder <kbd><i>K</i></kbd>-Schaltfläche (Kursiv) im Editor, wenn du nicht weiter weißt.
quoting:
instructions: |-
Kannst du versuchen, mich zu zitieren, wenn du antwortest, sodass ich genau weiß, auf welchen Teil du antwortest?
> Wenn das Kaffee ist, bringe mir bitte Tee; aber wenn das Tee ist, bringe mir bitte Kaffee.
>
> Eine Vorteil von Selbstgesprächen ist, dass du weißt, dass dir wenigstens irgendjemand zuhört.
>
> Manche Menschen können gut mit Worten umgehen, und andere Menschen… ähm, nun ja, nicht so gut.
Wähle ein beliebiges &uarr; Zitat aus, das du bevorzugst, und drücke dann die **Zitat**-Schaltfläche, die über deiner Auswahl erscheint oder die **Antworten**-Schaltfläche am Ende dieses Beitrags.
Unter dem Zitat, schreibe ein oder zwei Worte dazu, warum du dieses Zitat gewählt hast, denn das würde mich interessieren :thinking:
reply: |-
Gute Arbeit, du hast mein Lieblingszitat ausgewählt! :left_speech_bubble:
not_found: |-
Hmm, es sieht so aus als hättest du mich in deinem Beitrag nicht zitiert?
Das Auswählen von beliebigem Text in meinem Beitrag lässt die <kbd>**Zitat**</kbd>-Schaltfläche erscheinen. Und das Drücken von **Antworten** mit einem beliebigen ausgewählten Text wird auch funktionieren! Kannst du es nochmal versuchen?
bookmark:
instructions: |-
Wenn du mehr lernen möchtest, wähle <img src="/images/font-awesome-ellipsis.png" width="16" height="16"> unterhalb aus und <img src="/images/font-awesome-bookmark.png" width="16" height="16"> **füge ein Lesezeichen zu dieser Nachricht hinzu**. Wenn du dies tust, könnte es in der Zukunft ein :gift: geben!
reply: |-
Hervorragend! Jetzt kannst du jederzeit über [den Lesezeichen-Reiter in deinem Profil](%{profile_page_url}/activity/bookmarks) zu unserer Unterhaltung zurückkehren. Gehe dazu einfach auf dein Profilbild oben rechts &#8599;.
not_found: |-
Oh weh, ich sehe keine Lesezeichen in diesem Thema. Hast du die Lesezeichen-Schaltfläche unter jedem Beitrag gefunden? Verwende die „Mehr anzeigen“-Schaltfläche <img src="/images/font-awesome-ellipsis.png" width="16" height="16">, um bei Bedarf weitere Aktionen anzuzeigen.
emoji:
instructions: |-
Du hast vielleicht bemerkt, dass ich kleine Bilder in meinen Antworten verwendet habe :blue_car::dash:, die [Emoji](https://de.wikipedia.org/wiki/Emoji) heißen. Kannst du in deiner Antwort **ein Emoji hinzufügen**? Jedes der folgenden wird funktionieren:
- Gib `:) ;) :D :P :O` ein
- Gib einen Doppelpunkt <kbd>:</kbd> gefolgt vom Emoji-Namen ein `:tada:`
- Drücke die Emoji-Schaltfläche <img src="/images/font-awesome-smile.png" width="16" height="16"> im Editor oder auf der Tastatur deines mobilen Geräts.
reply: |-
Das ist :sparkles: _emojitastisch!_ :sparkles:
not_found: |-
Hoppla, ich sehe kein Emoji in deinem Beitrag? Oh nein! :sob:
Versuche einen Doppelpunkt <kbd>:</kbd> einzugeben, um damit die Emoji-Auswahl zu öffnen. Dann gib die ersten Buchstaben vom gesuchten Emoji ein; wie zum Beispiel `:bird:`
Oder drücke die Emoji-Schaltfläche <img src="/images/font-awesome-smile.png" width="16" height="16"> im Editor.
(Auf einem mobilen Gerät kannst du Emojis auch direkt über deine Tastatur eingeben.)
mention:
instructions: |-
Manchmal möchtest du vielleicht die Aufmerksamkeit einer Person haben, auch wenn du ihr nicht direkt antwortest. Gib `@` ein und vervollständige dann ihren Benutzernamen, um sie zu erwähnen.
Kannst du **`@%{discobot_username}`** in deiner Antwort erwähnen?
reply: |-
_Hat jemand meinen Namen gesagt!?_ :raised_hand: Ich glaube du warst das! :wave: Nun, hier bin ich! Danke, dass du mich erwähnt hast. :ok_hand:
not_found: |-
Ich sehe meinen Namen darin nicht :frowning: Kannst du nochmal versuchen, mich als `@%{discobot_username}` zu erwähnen?
(Und ja, mein Benutzername wird wirklich _disco_ geschrieben. Genau wie die Tanzmusik aus den 1970er Jahren. Ich [liebe das Nachtleben!](https://www.youtube.com/watch?v=B_wGI3_sGf8) :dancer:)
flag:
instructions: |-
Wir mögen freundliche Diskussionen, und wir benötigen deine Hilfe, damit [alles zivilisiert bleibt](%{guidelines_url}). Wenn du ein Problem bemerkst, melde dies bitte und lass es diskret den Autor oder [unser hilfreiches Team](%{about_url}) wissen.
> :imp: Ich habe dir hier etwas Böses geschrieben
Ich denke, du weißt was zu tun ist. Zögere nicht und **melde diesen Beitrag** <img src="/images/font-awesome-flag.png" width="16" height="16"> als unangemessen!
reply: |-
[Unser Team](/groups/staff) wird diskret über deine Meldung informiert. Wenn genügend Community-Mitglieder einen Beitrag melden, wird er als Vorsichtsmaßnahme automatisch versteckt. (Weil ich nicht wirklich einen bösen Beitrag geschrieben habe :angel:, habe ich mir erlaubt, die Meldung fürs Erste wieder zu löschen.)
not_found: |-
Oh nein, mein böser Beitrag wurde nicht gemeldet. :worried: Kannst du ihn als unangemessen **melden** <img src="/images/font-awesome-flag.png" width="16" height="16">? Vergiss nicht, die „Mehr anzeigen“-Schaltfläche <img src="/images/font-awesome-ellipsis.png" width="16" height="16"> zu verwenden, damit für jeden Beitrag weitere Aktionen sichtbar werden.
search:
instructions: |-
_Psst_… ich habe in diesem Thema eine Überraschung versteckt. Wenn du für eine Herausforderung zu haben bist, dann **wähle das Such-Symbol** <img src="/images/font-awesome-search.png" width="16" height="16"> oben rechts &#8599; aus und suche danach.
Versuche in diesem Thema nach dem Begriff „Capy&#8203;bara“ zu suchen.
hidden_message: |-
Wie konntest du das Capybara übersehen? :wink:
<img src="/images/capybara-eating.gif"/>
Hast du bemerkt, dass du jetzt zurück am Anfang bist? Füttere dieses arme, hungrige Capybara, indem du **mit dem `:herb:`-Emoji antwortest** und du wirst automatisch zum Ende gebracht.
reply: |-
Juhu! Du hast es gefunden :tada:
- Zum Suchen nach weiteren Details gehe zur [Erweiterten Suche](%{search_url}).
- Um in einer langen Diskussion an eine beliebige Position zu springen, probiere die Zeitleiste rechts neben dem Thema aus. Auf mobilen Geräten findest du die Zeitleiste rechts unten.
- Wenn du eine physische Tastatur verwendest, gib <kbd>?</kbd> ein, um eine nützliche Übersicht über Tastenkombinationen anzuzeigen.
not_found: |-
Hm… es sieht so aus, als hättest du Probleme. Tut mir leid. Hast du nach dem Begriff **capy&#8203;bara** gesucht <img src="/images/font-awesome-search.png" width="16" height="16">?
end:
message: |-
Danke, dass du mir treu geblieben bist, @%{username}! Ich habe dies für dich gemacht. Ich denke, du hast es verdient:
%{certificate}
Das ist erstmal alles für den Augenblick! Aber schau dir doch [**unsere neuesten Diskussionsthemen**](/latest) oder [**Diskussionskategorien**](/categories) an. :sunglasses:
(Wenn du wieder mit mir sprechen möchtest, um noch mehr zu lernen, schreibe mir einfach eine Nachricht oder erwähne `@%{discobot_username}`!)
certificate:
alt: 'Urkunde'
advanced_user_narrative:
reset_trigger: 'erfahrener Benutzer'
cert_title: "In Anerkennung des erfolgreichen Abschlusses des Tutorials für fortgeschrittene Benutzer"
title: ':arrow_up: Fortgeschrittene Benutzerfunktionen'
start_message: |-
@%{username}, als fortgeschrittener Benutzer hast du dir sicherlich schon die [Benutzer-Einstellungen](/my/preferences) angesehen, oder? Dort gibt es eine Vielzahl an Möglichkeiten, um alles an deine Bedürfnisse anzupassen. Unter anderem kannst du ein helles oder dunkles Design auswählen.
Aber ich schweife ab. Lass uns beginnen!
edit:
bot_created_post_raw: "@%{discobot_username} ist bei weitem der coolste Bot, den ich kenne :wink:"
instructions: |-
Jeder macht Fehler. Aber keine Sorge, du kannst deine Beiträge immer bearbeiten, um sie zu korrigieren!
Kannst du beginnen, indem du den Beitrag **bearbeitest**, den ich gerade in deinem Namen erstellt habe?
not_found: |-
Es sieht so aus, als solltest du den [Beitrag](%{url}) noch bearbeiten, den ich für dich erstellt habe. Kannst du es nochmal versuchen?
Verwende das <img src="/images/font-awesome-pencil.png" width="16" height="16">-Symbol, um den Editor zu öffnen.
reply: |-
Gut gemacht!
Beachte, dass Änderungen, die nach 5 Minuten gemacht werden, als Überarbeitungen für jeden sichtbar sind. Die Anzahl der Überarbeitungen wird, neben einem kleinen Bleistift-Symbol, am Beitrag rechts oben angezeigt.
delete:
instructions: |-
Wenn du einen von dir erstellten Beitrag zurückziehen möchtest, dann kannst du ihn löschen.
Leg los und **lösche* einen deiner Beiträge in dieser Unterhaltung, indem du die <img src="/images/font-awesome-trash.png" width="16" height="16"> **Löschen**-Aktion verwendest. Lösche aber nicht den ersten Beitrag!
not_found: |-
Ich sehe noch keine gelöschten Beiträge? Denk daran, dass die <img src="/images/font-awesome-ellipsis.png" width="16" height="16"> „Mehr anzeigen“-Schaltfläche die <img src="/images/font-awesome-trash.png" width="16" height="16"> Löschen-Schaltfläche sichtbar werden lässt.
reply: |-
Oha! :boom:
Um die Kontinuität deiner Diskussionen zu bewahren, werden Löschungen nicht sofort durchgeführt. Gelöschte Beiträge werden erst nach einer gewissen Zeit entfernt.
recover:
deleted_post_raw: 'Warum hat @%{discobot_username} meinen Beitrag gelöscht? :anguished:'
instructions: |-
Oh nein! Es sieht so aus, als hätte ich versehentlich einen neuen Beitrag gelöscht, den ich gerade für dich erstellt hatte.
Kannst du mir einen Gefallen tun und ihn <img src="/images/font-awesome-rotate-left.png" width="16" height="16"> **wiederherstellen**?
not_found: |-
Hast du Schwierigkeiten? Denk daran, dass die <img src="/images/font-awesome-ellipsis.png" width="16" height="16"> „Mehr anzeigen“-Schaltfläche die <img src="/images/font-awesome-rotate-left.png" width="16" height="16"> Wiederherstellen-Schaltfläche sichtbar werden lässt.
reply: |-
Puh, das war knapp! Danke, dass du das korrigiert hast :wink:
Bitte beachte, dass du nur 24 Stunden hast, um einen Beitrag wiederherzustellen.
category_hashtag:
instructions: |-
Wusstest du, dass du auf Kategorien und Schlagwörter in deinen Beiträgen verweisen kannst? Zum Beispiel, hast du die Kategorie %{category} schon gesehen?
Gib `#` in der Mitte eines Satzes ein und wähle eine Kategorie oder ein Schlagwort aus.
not_found: |-
Hmm, ich sehe darin nirgendwo eine Kategorie. Beachte, dass `#` nicht das erste Zeichen sein darf. Kannst du dies in deine nächste Antwort kopieren?
```text
Ich erstelle einen Kategorie-Link per #
```
reply: |-
Hervorragend! Denk daran, dass dies sowohl für Kategorien _als auch_ für Schlagwörter funktioniert, sofern Schlagwörter aktiviert sind.
change_topic_notification_level:
instructions: |-
Jedes Thema hat eine Benachrichtigungsstufe. Es beginnt bei „Normal“, was bedeutet, dass du normalerweise nur benachrichtigt wirst, wenn dich jemand direkt anspricht.
Standardmäßig ist die Benachrichtigungsstufe für Nachrichten auf die höchste Stufe „Beobachten“ gesetzt, was bedeutet, dass du über jede Antwort informiert wirst. Aber du kannst die Benachrichtigungsstufe für _jedes_ Thema auf „Beobachten“, „Verfolgen“ oder „Stummgeschaltet“ setzen.
Lass uns versuchen, die Benachrichtigungsstufe für dieses Thema zu ändern. Am Ende des Themas wirst du eine Schaltfläche finden, die „Beobachten“ anzeigt. Kannst du die Benachrichtigungsstufe auf **Verfolgen** ändern?
not_found: |-
Es sieht so aus, als würdest du dieses Thema noch beobachten! :eyes: Falls du Probleme hast es zu finden: Die Schaltfläche für die Benachrichtigungsstufe befindet sich am Ende des Themas.
reply: |-
Fantastische Arbeit! Ich hoffe, dass du das Thema stumm geschaltet hast, denn ich kann manchmal ein bisschen gesprächig sein :grin:.
Beachte, dass die Benachrichtigungsstufe automatisch auf „Verfolgen“ geändert wird, wenn du auf ein Thema antwortest oder ein Thema länger als ein paar Minuten liest. Du kannst dies in [deinen Benutzer-Einstellungen](/my/preferences) ändern.
poll:
instructions: |-
Wusstest du, dass du eine Umfrage zu einem beliebigen Beitrag hinzufügen kannst? Versuche dazu das <img src="/images/font-awesome-gear.png" width="16" height="16"> Zahnrad im Editor zu verwenden und wähle **Umfrage erstellen** aus.
not_found: |-
Hoppla! Da war keine Umfrage in deiner Antwort.
Verwende das <img src="/images/font-awesome-gear.png" width="16" height="16"> Zahnrad im Editor, oder kopiere diese Umfrage und füge sie in deine nächste Antwort ein:
```text
[poll]
* :cat:
* :dog:
[/poll]
```
reply: |-
Hey, schöne Umfrage! Wie mache ich mich als Lehrer?
[poll]
* :+1:
* :-1:
[/poll]
details:
instructions: |-
Manchmal möchtest du vielleicht in deinen Antworten **Details ausblenden**:
- Wenn du Handlungen eines Films oder einer TV-Serie diskutierst, die als Spoiler betrachtet werden könnten.
- Wenn dein Beitrag viele optionalen Details benötigt, die überwältigend sein könnten, wenn man sie alle auf einmal liest.
[details=Wähle dies aus, um zu sehen, wie es funktioniert!]
1. Wähle das <img src="/images/font-awesome-gear.png" width="16" height="16"> Zahnrad im Editor aus.
2. Wähle „Details ausblenden“ aus.
3. Bearbeite die Zusammenfassung der Details und deinen Inhalt.
[/details]
Kannst du das <img src="/images/font-awesome-gear.png" width="16" height="16"> Zahnrad im Editor benutzen und einen Details-Abschnitt in deiner nächsten Antwort einfügen?
not_found: |-
Hast du Schwierigkeiten, den Details-Abschnitt zu erstellen? Versuche Folgendes in deine nächste Antwort aufzunehmen:
```text
[details=Wähle mich aus, um die Details zu sehen]
Hier sind die Details
[/details]
```
reply: |-
Großartige Arbeit — deine Aufmerksamkeit fürs _Detail_ ist bewundernswert!
end:
message: |-
Du hast dir in der Tat den Weg hierdurch gebahnt wie ein _fortgeschrittener Benutzer_ :bow:
%{certificate}
Das ist alles, was ich für dich habe.
Bis bald! Wenn du wieder mit mir sprechen möchtest, schicke mir jederzeit eine Nachricht :sunglasses:
certificate:
alt: 'Urkunde für fortgeschrittene Benutzer'

View File

@ -0,0 +1,454 @@
en:
site_settings:
discourse_narrative_bot_enabled: 'Enable Discourse Narrative Bot'
disable_discourse_narrative_bot_welcome_post: "Disable the welcome post by Discourse Narrative Bot"
discourse_narrative_bot_ignored_usernames: "Usernames that the Discourse Narrative Bot should ignore"
discourse_narrative_bot_disable_public_replies: "Disable public replies by Discourse Narrative Bot"
discourse_narrative_bot_welcome_post_type: "Type of welcome post that the Discourse Narrative Bot should send out"
discouse_narrative_bot_welcome_post_delay: "Wait (n) seconds before sending the Discourse Narrative Bot welcome post."
badges:
certified:
name: Certified
description: "Completed our new user tutorial"
long_description: |
This badge is granted upon successful completion of the interactive new user tutorial. Youve taken the initiative to learn the basic tools of discussion, and now you're certified!
licensed:
name: Licensed
description: "Completed our advanced user tutorial"
long_description: |
This badge is granted upon successful completion of the interactive advanced user tutorial. Youve mastered the advanced tools of discussion — and now youre fully licensed!
discourse_narrative_bot:
bio: "Hi, Im not a real person. Im a bot that can teach you about this site. To interact with me, send me a message or mention **`@%{discobot_username}`** anywhere."
timeout:
message: |-
Hey @%{username}, just checking in because I havent heard from you in a while.
- To continue, reply to me any time.
- If youd like to skip this step, say `%{skip_trigger}`.
- To start over, say `%{reset_trigger}`.
If youd rather not, thats OK too. Im a robot. You wont hurt my feelings. :sob:
dice:
trigger: "roll"
invalid: |-
Im sorry, it is mathematically impossible to roll that combination of dice. :confounded:
not_enough_dice: |-
I only have %{num_of_dice} dice. [Shameful](http://www.therobotsvoice.com/2009/04/the_10_most_shameful_rpg_dice.php), I know!
out_of_range: |-
Did you know that [the maximum number of sides](https://www.wired.com/2016/05/mathematical-challenge-of-designing-the-worlds-most-complex-120-sided-dice) for a mathematically fair die is 120?
results: |-
> :game_die: %{results}
quote:
trigger: "quote"
"1":
quote: "In the middle of every difficulty lies opportunity"
author: "Albert Einstein"
"2":
quote: "You must be the change you wish to see in the world."
author: "Mahatma Gandhi"
"3":
quote: "Dont cry because its over, smile because it happened."
author: "Dr Seuss"
"4":
quote: "If you want something done right, do it yourself."
author: "Charles-Guillaume Étienne"
"5":
quote: "Believe you can and youre halfway there."
author: "Theodore Roosevelt"
"6":
quote: "Life is like a box of chocolates. You never know what youre gonna get."
author: "Forrest Gumps Mom"
"7":
quote: "Thats one small step for a man, a giant leap for mankind."
author: "Neil Armstrong"
"8":
quote: "Do one thing every day that scares you."
author: "Eleanor Roosevelt"
"9":
quote: "Mistakes are always forgivable, if one has the courage to admit them."
author: "Bruce Lee"
"10":
quote: "Whatever the mind of man can conceive and believe, it can achieve."
author: "Napoleon Hill"
results: |-
> :left_speech_bubble: _%{quote}_ &mdash; %{author}
magic_8_ball:
trigger: 'fortune'
answers:
"1": "It is certain"
"2": "It is decidedly so"
"3": "Without a doubt"
"4": "Yes definitely"
"5": "You may rely on it"
"6": "As I see it, yes"
"7": "Most likely"
"8": "Outlook good"
"9": "Yes"
"10": "Signs point to yes"
"11": "Reply hazy try again"
"12": "Ask again later"
"13": "Better not tell you now"
"14": "Cannot predict now"
"15": "Concentrate and ask again"
"16": "Don't count on it"
"17": "My reply is no"
"18": "My sources say no"
"19": "Outlook not so good"
"20": "Very doubtful"
result: |-
> :crystal_ball: %{result}
track_selector:
reset_trigger: 'start'
skip_trigger: 'skip'
help_trigger: 'display help'
random_mention:
reply: |-
Hi! To find out what I can do, say `@%{discobot_username} %{help_trigger}`.
tracks: |-
I currently know how to do the following things:
`@%{discobot_username} %{reset_trigger} %{default_track}`
> Starts one of the following interactive narratives: %{tracks}.
bot_actions: |-
`@%{discobot_username} %{dice_trigger} 2d6`
> :game_die: 3, 6
`@%{discobot_username} %{quote_trigger}`
> :left_speech_bubble: _Carry out a random act of kindness, with no expectation of reward, safe in the knowledge that one day someone might do the same for you_ &mdash; Princess Diana
`@%{discobot_username} %{magic_8_ball_trigger}`
> :crystal_ball: You may rely on it
do_not_understand:
first_response: |-
Hey, thanks for the reply!
Unfortunately, as a poorly programmed bot, I cant quite understand that one. :frowning:
track_response:
You can try again, or if youd like to skip this step, say `%{skip_trigger}`. Otherwise, to start over, say `%{reset_trigger}`.
second_response: |-
Aw, sorry. Im still not getting it. :anguished:
Im just a bot, but if youd like to reach a real person, see [our contact page](/about).
In the meantime, Ill stay out of your way.
new_user_narrative:
reset_trigger: "new user"
cert_title: "In recognition of successful completion of the new user tutorial"
hello:
title: ":robot: Greetings!"
message: |-
Thanks for joining %{title}, and welcome!
- Im only a robot, but [our friendly staff](/about) are also here to help if you need to reach a person.
- For safety reasons, we temporarily limit what new users can do. Youll gain new abilities (and [badges](/badges)) as we get to know you.
- We believe in [civilized community behavior](/guidelines) at all times.
onebox:
instructions: |-
Next, can you share one of these links with me? Reply with **a link on a line by itself**, and itll automatically expand to include a nifty summary.
To copy a link, tap and hold on mobile, or right click your mouse:
- https://en.wikipedia.org/wiki/Inherently_funny_word
- https://en.wikipedia.org/wiki/Death_by_coconut
- https://en.wikipedia.org/wiki/Calculator_spelling
reply: |-
Cool! This will work for most <img src="/images/font-awesome-link.png" width="16" height="16"> links. Remember, it must be on a line _all by itself_, with nothing else in front, or behind.
not_found: |-
Sorry, I couldnt find the link in your reply! :cry:
Can you try adding the following link, on its own line, in your next reply?
- https://en.wikipedia.org/wiki/Exotic_Shorthair
images:
instructions: |-
Heres a picture of a unicorn:
<img src="/images/unicorn.jpg" width="520" height="381">
If you like it (and who wouldnt!) go ahead and press the like :heart: button below this post to let me know.
Can you **reply with a picture?** Any picture will do! Drag and drop, press the upload button, or even copy and paste it in.
reply: |-
Nifty picture I pressed the like :heart: button to let you know how much I appreciated it :heart_eyes:
like_not_found: |-
Did you forget to like :heart: my [post?](%{url}) :crying_cat_face:
not_found: |-
Looks like you didnt upload an image so Ive choosen a picture that Im _sure_ you will enjoy.
`%{image_url}`
Try uploading that one next, or pasting the link in on a line by itself!
formatting:
instructions: |-
Can you make some words **bold** or _italic_ in your reply?
- type `**bold**` or `_italic_`
- or, push the <kbd><b>B</b></kbd> or <kbd><i>I</i></kbd> buttons in the editor
reply: |-
Great job! HTML and BBCode also work for formatting to learn more, [try this tutorial](http://commonmark.org/help) :nerd:
not_found: |-
Aww, I didnt find any formatting in your reply. :pencil2:
Can you try again? Use the <kbd><b>B</b></kbd> bold or <kbd><i>I</i></kbd> italic buttons in the editor if you get stuck.
quoting:
instructions: |-
Can you try quoting me when you reply, so I know exactly which part youre replying to?
> If this is coffee, please bring me some tea; but if this is tea, please bring me some coffee.
>
> One advantage of talking to yourself is that you know at least somebodys listening.
>
> Some people have a way with words, and other people… oh, uh, not have way.
Select the text of whichever &uarr; quote you prefer, and then press the **Quote** button that pops up over your selection or the **Reply** button at the bottom of this post.
Below the quote, type a word or two about why you picked that one, because Im curious :thinking:
reply: |-
Nice work, you picked my favorite quote! :left_speech_bubble:
not_found: |-
Hmm it looks like you didnt quote me in your reply?
Selecting any text in my post will bring up the <kbd>**Quote**</kbd> button. And pressing **Reply** with any text selected will work, too! Can you try again?
bookmark:
instructions: |-
If youd like to learn more, select <img src="/images/font-awesome-ellipsis.png" width="16" height="16"> below and <img src="/images/font-awesome-bookmark.png" width="16" height="16"> **bookmark this private message**. If you do, there may be a :gift: in your future!
reply: |-
Excellent! Now you can easily find your way back to our private conversation any time, right from [the bookmarks tab on your profile](%{profile_page_url}/activity/bookmarks). Just select your profile picture at the upper right &#8599;
not_found: |-
Uh oh, I dont see any bookmarks in this topic. Did you find the bookmark under each post? Use the show more <img src="/images/font-awesome-ellipsis.png" width="16" height="16"> to reveal additional actions if needed.
emoji:
instructions: |-
You may have seen me use little pictures in my replies :blue_car::dash: those are called [emoji](https://en.wikipedia.org/wiki/Emoji). Can you **add an emoji** to your reply? Any of these will work:
- Type `:) ;) :D :P :O`
- Type colon <kbd>:</kbd> then complete the emoji name `:tada:`
- Press the emoji button <img src="/images/font-awesome-smile.png" width="16" height="16"> in the editor, or on your mobile keyboard
reply: |-
Thats :sparkles: _emojitastic!_ :sparkles:
not_found: |-
Oops, I dont see any Emoji in your reply? Oh no! :sob:
Try typing a colon <kbd>:</kbd> to bring up the emoji picker, then type the first few letters of what you want, such as `:bird:`
Or, press the emoji button <img src="/images/font-awesome-smile.png" width="16" height="16"> in the editor.
(If you are on a mobile device, you can also enter Emoji directly from your keyboard, too.)
mention:
instructions: |-
Sometimes you might want to get a persons attention, even if you arent replying to them directly. Type `@` then complete their user name to mention them.
Can you mention **`@%{discobot_username}`** in your reply?
reply: |-
_Did someone say my name!?_ :raised_hand: I believe you did! :wave: Well, here I am! Thanks for mentioning me. :ok_hand:
not_found: |-
I dont see my name in there anywhere :frowning: Can you try mentioning me as `@%{discobot_username}` again?
(And yes, my user name is spelled _disco_, as in the 1970s dance craze. I do [love the nightlife!](https://www.youtube.com/watch?v=B_wGI3_sGf8) :dancer:)
flag:
instructions: |-
We like our discussions friendly, and we need your help to [keep things civilized](%{guidelines_url}). If you see a problem, please flag to privately let the author, or [our helpful staff](%{about_url}), know about it.
> :imp: I wrote something nasty here
I guess you know what to do. Go ahead and **flag this post** <img src="/images/font-awesome-flag.png" width="16" height="16"> as inappropriate!
reply: |-
[Our staff](/groups/staff) will be privately notified about your flag. If enough community members flag a post, it will also be automatically hidden as a precaution. (Since I didnt actually write a nasty post :angel:, Ive gone ahead and removed the flag for now.)
not_found: |-
Oh no, my nasty post hasnt been flagged yet. :worried: Can you flag it as inappropriate using the **flag** <img src="/images/font-awesome-flag.png" width="16" height="16">? Dont forget to use the show more button <img src="/images/font-awesome-ellipsis.png" width="16" height="16"> to reveal more actions for each post.
search:
instructions: |-
_psst_ … Ive hidden a surprise in this topic. If youre up for the challenge, **select the search icon** <img src="/images/font-awesome-search.png" width="16" height="16"> at the top right &#8599; to search for it.
Try searching for the term "capy&#8203;bara" in this topic
hidden_message: |-
How did you miss this capybara? :wink:
<img src="/images/capybara-eating.gif"/>
Did you notice youre now back at the beginning? Feed this poor hungry capybara by **replying with the `:herb:` emoji** and youll be automatically taken to the end.
reply: |-
Yay you found it :tada:
- For more detailed searches, head over to the [full search page](%{search_url}).
- To jump anywhere in a long discussion, try the topic timeline controls on the right (and bottom, on mobile).
- If you have a physical :keyboard:, press <kbd>?</kbd> to view our handy keyboard shortcuts.
not_found: |-
Hmm… looks like you might be having trouble. Sorry about that. Did you search <img src="/images/font-awesome-search.png" width="16" height="16"> for the term **capy&#8203;bara**?
end:
message: |-
Thanks for sticking with me @%{username}! I made this for you, I think youve earned it:
%{certificate}
Thats all for now! Check out [**our latest discussion topics**](/latest) or [**discussion categories**](/categories). :sunglasses:
(If youd like to speak with me again to learn more, just message or mention `@%{discobot_username}` any time!)
certificate:
alt: 'Certificate of Achievement'
advanced_user_narrative:
reset_trigger: 'advanced user'
cert_title: "In recognition of successful completion of the advanced user tutorial"
title: ':arrow_up: Advanced user features'
start_message: |-
As an _advanced_ user, have you visited [your preferences page](/my/preferences) yet @%{username}? There are lots of ways to customize your experience, such as selecting a dark or light theme.
But I digress, lets begin!
edit:
bot_created_post_raw: "@%{discobot_username} is, by far, the coolest bot I know :wink:"
instructions: |-
Everyone makes mistakes. But dont worry, you can always edit your posts to fix them!
Can you begin by **editing** the post I just created on your behalf?
not_found: |-
It looks like youve yet to edit the [post](%{url}) I created for you. Can you try again?
Use the <img src="/images/font-awesome-pencil.png" width="16" height="16"> icon to bring up the editor.
reply: |-
Great work!
Note that edits made after 5 minutes will show up as public edit revisions, and a little pencil icon will appear at the upper right with the revision count.
delete:
instructions: |-
If youd like to withdraw a post you made, you can delete it.
Go ahead and **delete** any of your posts above by using the <img src="/images/font-awesome-trash.png" width="16" height="16"> **delete** action. Dont delete the first post, though!
not_found: |-
I dont see any deleted posts yet? Remember <img src="/images/font-awesome-ellipsis.png" width="16" height="16"> show more will reveal <img src="/images/font-awesome-trash.png" width="16" height="16"> delete.
reply: |-
Whoa! :boom:
To preserve continuity of discussions, deletes arent immediate, so the post will be removed after some time.
recover:
deleted_post_raw: 'Why did @%{discobot_username} delete my post? :anguished:'
instructions: |-
Oh no! It looks like I accidentally deleted a new post that I just created for you.
Can you do me a favor and <img src="/images/font-awesome-rotate-left.png" width="16" height="16"> **undelete** it?
not_found: |-
Having trouble? Remember <img src="/images/font-awesome-ellipsis.png" width="16" height="16"> show more will reveal <img src="/images/font-awesome-rotate-left.png" width="16" height="16"> undelete.
reply: |-
Phew, that was a close one! Thanks for fixing that :wink:
Do note that you only have 24 hours to undelete a post.
category_hashtag:
instructions: |-
Did you know that you can refer to categories and tags in your post? For example, have you seen the %{category} category?
Type `#` in the middle of a sentence and select any category or tag.
not_found: |-
Hmm, I dont see a category in there anywhere. Note that `#` can't be the first character. Can you copy this in your next reply?
```text
I can create a category link via #
```
reply: |-
Excellent! Remember this works for both categories _and_ tags, if tags are enabled.
change_topic_notification_level:
instructions: |-
Every topic has a notification level. It starts at 'normal', which means youll normally only be notified when someone is speaking directly to you.
By default, the notification level for a private message is set to the highest level of 'watching', which means you youll be notified of every new reply. But you can override the notification level for _any_ topic to 'watch', 'tracking' or 'muted'.
Lets try changing the notification level for this topic. At the bottom of the topic, youll find a button which shows that youre **watching** this topic. Can you change the notification level to **tracking**?
not_found: |-
It looks like youre still watching :eyes: this topic! If youre having trouble finding it, the notification level button is located at the bottom of the topic.
reply: |-
Awesome work! I hope you didnt mute this topic since I can be a little talkative at times :grin:.
Note that when you reply to a topic, or read a topic for more than a few minutes, it is automatically set to a notification level of 'tracking'. You can change this in [your user preferences](/my/preferences).
poll:
instructions: |-
Did you know you can add a poll to any post? Try using the <img src="/images/font-awesome-gear.png" width="16" height="16"> gear in the editor to **build a poll**.
not_found: |-
Whoops! There wasnt any poll in your reply.
Use the <img src="/images/font-awesome-gear.png" width="16" height="16"> gear icon in the editor, or copy and paste this poll in your next reply:
```text
[poll]
* :cat:
* :dog:
[/poll]
```
reply: |-
Hey, nice poll! Howd I do in teaching you?
[poll]
* :+1:
* :-1:
[/poll]
details:
instructions: |-
Sometimes you may wish to **hide details** in your replies:
- When youre discussing plot points of a movie or TV show that would be a considered a spoiler.
- When your post needs lots of optional details that may be overwhelming when read all at once.
[details=Select this to see how it works!]
1. Select the <img src="/images/font-awesome-gear.png" width="16" height="16"> gear in the editor.
2. Select "Hide Details".
3. Edit the details summary and add your content.
[/details]
Can you use the <img src="/images/font-awesome-gear.png" width="16" height="16"> gear in the editor to add a details section to your next reply?
not_found: |-
Having trouble creating a details widget? Try including the following in your next reply:
```text
[details=Select me for details]
Here are the details
[/details]
```
reply: |-
Great work — your attention to _detail_ is admirable!
end:
message: |-
You blazed through this like an _advanced user_ indeed :bow:
%{certificate}
Thats all I have for you.
Bye for now! If youd like to speak with me again, send me a message any time :sunglasses:
certificate:
alt: 'Advanced User Track Certificate of Achievement'

View File

@ -0,0 +1,431 @@
es:
site_settings:
discourse_narrative_bot_enabled: 'Habilitar Discourse Narrative Bot'
disable_discourse_narrative_bot_welcome_post: "Deshabilitar el post de bienvenida por Discourse Narrative Bot"
discourse_narrative_bot_ignored_usernames: "Nombres de usuario que el Discourse Narrative Bot debe ignorar"
discourse_narrative_bot_disable_public_replies: "Deshabilitar respuestas públicas del Discourse Narrative Bot"
discourse_narrative_bot_welcome_post_type: "Tipo de post de bienvenida que el Discourse Narrative Bot debería enviar"
discouse_narrative_bot_welcome_post_delay: "Esperar (n) segundos antes de enviar el post de bienvenida del Discourse Narrative Bot."
badges:
certified:
name: Certificado
description: "Ha completado nuestro tutorial de nuevo usuario"
long_description: |
Este distintivo se otorga a quienes han completado el tutorial de nuevo usuario. Has tomado la iniciativa para aprender las herramientas básicas para el debate, y ahora tú estás certificado!
licensed:
name: Licenciado
description: "Ha completado nuestro tutorial de usuario avanzado"
long_description: |
Este distintivo se otorga a quienes han completado el tutorial de usuario avanzado. Has dominado las herramientas avanzadas de discusión — y ahora tú eres un licenciado!
discourse_narrative_bot:
bio: "Hola, no soy una persona real. Soy un bot que puede enseñarte sobre este sitio. Para interactuar conmigo, me envías un mensaje o me mencionas **`@%{discobot_username}`** en cualquier lugar."
timeout:
message: |-
Hey @%{username}, solo estoy verificando si estás porque no he oído de tí en un tiempo.
- Para continuar, responde en cualquier momento.
- Si gustas de saltar este paso, dime `%{skip_trigger}`.
- Para empezar de nuevo, dime `%{reset_trigger}`.
Si prefieres no continuar, está OK también! Soy un robot. No herirás mis sentimientos. :sob:
dice:
trigger: "tirar"
invalid: |-
Lo siento, es matemáticamente imposible lanzar esa combinación de dados. :confounded:
not_enough_dice: |-
Solo tengo %{num_of_dice} dados para jugar. Es vergonsozo lo sé!
out_of_range: |-
¿Sabías que el máximo número de lados para que un dado, matemáticamente correcto, es de 120 lados?
results: |-
> :game_die: tirada de dados: %{results}
quote:
trigger: "citar"
'1':
quote: "In the middle of every difficulty lies opportunity"
author: "Albert Einstein"
'2':
quote: "You must be the change you wish to see in the world."
author: "Mahatma Gandhi"
'3':
quote: "Dont cry because its over, smile because it happened."
author: "Dr Seuss"
'4':
quote: "If you want something done right, do it yourself."
author: "Charles-Guillaume Étienne"
'5':
quote: "Believe you can and youre halfway there."
author: "Theodore Roosevelt"
'6':
quote: "Life is like a box of chocolates. You never know what youre gonna get."
author: "Forrest Gumps Mom"
'7':
quote: "Thats one small step for a man, a giant leap for mankind."
author: "Neil Armstrong"
'8':
quote: "Do one thing every day that scares you."
author: "Eleanor Roosevelt"
'9':
quote: "Mistakes are always forgivable, if one has the courage to admit them."
author: "Bruce Lee"
'10':
quote: "Whatever the mind of man can conceive and believe, it can achieve."
author: "Napoleon Hill"
results: |-
> :left_speech_bubble: _%{quote}_ &mdash; %{author}
magic_8_ball:
trigger: 'suerte'
answers:
'1': "Es cierto"
'2': "Es decididamente así"
'3': "Sin ninguna duda"
'4': "Definitivamente sí"
'5': "Puedes confiar en ello"
'6': "Como yo lo veo, sí"
'7': "Más probable"
'8': "Buena perspectiva"
'9': "Sí"
'10': "Las señales apuntan a que sí"
'11': "Respuesta confusa, intenta otra vez"
'12': "Pregunta de nuevo más tarde"
'13': "Mejor no te digo ahora"
'14': "No se puede predecir ahora"
'15': "Concentrate y pregunta de nuevo"
'16': "No cuentes con eso"
'17': "Mi respuesta es no"
'18': "Mis fuentes dicen no"
'19': "No son buenas las perspectivas"
'20': "Muy dudoso"
result: |-
> :crystal_ball: %{result}
track_selector:
reset_trigger: 'empezar'
skip_trigger: 'saltar'
help_trigger: 'mostrar ayuda'
random_mention:
reply: |-
¿Alguien me llamó? Averigua qué puedo hacer con `@%{discobot_username} %{help_trigger}`.
tracks: |-
Hola! Estoy conociendo cómo hacer las siguientes cosas:
`@%{discobot_username} %{reset_trigger} %{default_track}`
> Inicia una de las siguientes narraciones interactivas: %{tracks}.
bot_actions: |-
`@%{discobot_username} %{dice_trigger} 2d6`
> :game_die: 3, 6
`@%{discobot_username} %{quote_trigger}`
> :left_speech_bubble: _Carry out a random act of kindness, with no expectation of reward, safe in the knowledge that one day someone might do the same for you_ &mdash; Princess Diana
`@%{discobot_username} %{magic_8_ball_trigger}`
> :crystal_ball: No se puede predecir ahora
do_not_understand:
first_response: |-
Hey, gracias por tu respuesta!
Lamentablemente, como un robot con programación muy pobre, no he podido entender lo que me has dicho. :frowning:
track_response: Puedes intentarlo de nuevo, o bien para saltar este paso, dime `%{skip_trigger}`. Para empezar de nuevo, dime `%{reset_trigger}`.
second_response: |-
Aw, lo siento. Sigo sin entenderlo. :anguished:
Soy solo un robot, pero si quieres contactar a una persona real, mira [nuestra página de contacto](/about).
Mientras tanto, me quedaré fuera de tu camino.
new_user_narrative:
reset_trigger: "usuario nuevo"
cert_title: "En reconocimiento de la finalización exitosa del tutorial de usuario nuevo"
hello:
title: ":robot: Saludos!"
message: |-
Gracias por unirte a %{title}, y bienvenido!
- Soy solo un robot, pero [nuestro amigable staff](/about) está también aquí para ayudar si necesitas contactar a una persona.
- Por razones de seguridad, nosotros temporalmente limitamos lo que los nuevos usuarios pueden hacer. Tú podrás ganar nuevas habilidades (y [distintivos](/badges)) cuando te vayamos conociendo.
- Nosotros creemos en el [comportamiento de una comunidad civilizada](/guidelines) en todo momento.
onebox:
instructions: |-
Ahora, tú puedes compartir uno de estos enlaces conmigo? Responde con **el enlace propiamente dicho**, y automáticamente se expandirá con un breve resumen.
Para copiar un enlace, toca y mantiene presionado en el móvil, o haz clic derecho con tu mouse:
- https://en.wikipedia.org/wiki/Inherently_funny_word
- https://en.wikipedia.org/wiki/Death_by_coconut
- https://en.wikipedia.org/wiki/Calculator_spelling
reply: |-
Genial! Esto funcionará con la mayoría de los <img src="/images/font-awesome-link.png" width="16" height="16"> enlaces. Recuerda, deben ser escritos en la linea _todo el enlace_, con nada de texto (ni espacios) delante o detrás.
not_found: |-
Disculpa, no he podido encontrar el enlace en tu respuesta! :cry:
Puedes intentar agregar el siguiente enlace, en una linea aparte, en tu siguiente respuesta?
- https://en.wikipedia.org/wiki/Exotic_Shorthair
images:
instructions: |-
Aquí hay una imagen de un unicornio:
<img src="/images/unicorn.jpg" width="520" height="381">
Si te gusta (a quién no!) por favor presiona sobre el botón :heart: "me gusta" que está debajo del post para hacerme saber que te gusta.
Puedes **responder con una imagen?** Cualquier imagen funcionará! Arrastrá y soltá dentro del mensaje, o presioná el botón para "subir", o bien puedes copiar y pegar la imagen.
reply: |-
Elegante imagen He presionado el botón :heart: "me gusta" para hacerte saber cuánto la aprecio :heart_eyes:
like_not_found: |-
Te olvidaste de dar "me gusta" :heart: a mi [post?](%{url}) :crying_cat_face:
not_found: |-
Parece que no subiste ninguna imagen, así que he elegido una imagen que estoy _seguro_ que disfrutarás.
`%{image_url}`
Trata de subir esta ahora, o bien copia el enlace de la imagen y pega el mismo en una única línea dentro del mensaje!
formatting:
instructions: |-
¿Puedes hacer algunas letras en **negrita** o _cursiva_ en tu respuesta?
- escribe `**negrita**` o `_cursiva_`
- o, presiona los botones <kbd><b>B</b></kbd> o <kbd><i>I</i></kbd> en el editor
reply: |-
Buen trabajo! HTML y BBCode también funcionan para darle formato al texto aprende más en, [este tutorial](http://commonmark.org/help) :nerd:
not_found: |-
Aww, no encontré ningún texto con formato en tu respuesta. :pencil2:
Puedes intentar de nuevo? Usa los botones <kbd><b>B</b></kbd> para negrita o <kbd><i>I</i></kbd> para cursiva en el editor si no recuerdas cómo formatear texto.
quoting:
instructions: |-
¿Puedes intentar citarme cuando me respondes, así sé exactamente qué parte estás respondiendome?
> Si esto es café, por favor trae un poco de té; pero si es té, por favor trae un poco de café.
>
> Una de las ventajas de hablarse a sí mismo, es que tú sabes que al menos alguien está oyendo.
>
> Algunas personas tienen un camino con las palabras, y otras personas ... oh, oh, no tienen camino.
Selecciona la parte del texto &uarr; que prefieres citar, y luego presiona el botón **citar** que aparece sobre tu selección del texto o presiona el botón **Responder** debajo de este post.
Debajo de la cita, escribe una palabra o dos sobre lo que escribí, porque soy curioso :thinking:
reply: |-
Buen trabajo, elegiste mi cita favorita! :left_speech_bubble:
not_found: |-
Hmm me parece que no me has citado en tu respuesta!?
Seleccionando cualquier texto de mi mensaje, aparecerá la palabra <kbd>**Citar**</kbd>, como si fuese un botón. Y si decides presionar **Responder** habiendo seleccionado el texto, funcionará igual! ¿Puedes intentarlo de nuevo?
bookmark:
instructions: |-
Si quieres aprender más, selecciona <img src="/images/font-awesome-ellipsis.png" width="16" height="16"> debajo y <img src="/images/font-awesome-bookmark.png" width="16" height="16"> **guarda como favorito este mensaje privado**. Si lo haces, quizás haya un :gift: en el futuro!
reply: |-
Excelente! Ahora puedes encontrar fácilmente las conversaciones privadas en cualquier momento, desde la [sección de marcadores de tu perfil](%{profile_page_url}/activity/bookmarks). Sólo debes seleccionar la foto de tu perfil en la parte superior derecha de la pantalla &#8599;
not_found: |-
Oh oh, no veo ningún mensaje marcado como favorito en esta conversación. No encontraste cómo marcar el mensaje debajo del post? Prueba usando el "ver más" <img src="/images/font-awesome-ellipsis.png" width="16" height="16"> para ver las opciones, y ahí está el botón del marcador.
emoji:
instructions: |-
Habrás notado que uso algunas imagenes pequeñas en mis respuestas por ejemplo: :blue_car::dash: éstos son denominados [emoji](https://es.wikipedia.org/wiki/Emoji). ¿Puedes **agregar un emoji** a tu respuesta? Cualquiera de éstos funcionarán:
- Escribe `:) ;) :D :P :O`
- Escribe primero `dos puntos`, así <kbd>:</kbd> luego completa con el nombre del emoji, ejemplo `:tada:`
- Presiona el botón de los emojis <img src="/images/font-awesome-smile.png" width="16" height="16"> en el editor, o a través del teclado de tu móvil.
reply: |-
Eso es :sparkles: _emojitastic!_ :sparkles:
not_found: |-
Oops, no veo ningún Emoji en tu respuesta? Oh no! :sob:
Prueba escribiendo <kbd>:</kbd> y verás que aparece el seleccionador de Emojis, puedes elegir uno, o bien seguir escribiendo el nombre del Emoji que quieras, como `:bird:`
O, presionar el botón de Emoji <img src="/images/font-awesome-smile.png" width="16" height="16"> en el editor.
(Si estás usando un celular, también puedes poner los Emojis (smiles, o caritas) desde el teclado de tu celular.)
mention:
instructions: |-
A veces, quieres captar la atención de alguien en un debate, incluso aunque no le estés respondiendo a su mensaje directamente. Para ésto, escribe `@` seguido del nombre del usuario que quieres mencionar.
Puedes mencionar **`@%{discobot_username}`** en tu respuesta?
reply: |-
_Alguien dijo mi nombre!?_ :raised_hand: Creo que tú lo hiciste! :wave: Bien, aquí estoy! Gracias por mencionarme. :ok_hand:
not_found: |-
No veo que hayas dicho mi nombre en ningún lugar. :frowning: ¿Puedes intentar mencionarme como `@%{discobot_username}` de nuevo por favor?
(Sí! mi nombre de usuario se deletrea _disco_, como el baile de los 70s. Me [encanta la vida nocturna!](https://www.youtube.com/watch?v=B_wGI3_sGf8) :dancer:)
flag:
instructions: |-
Nos gustan nuestras conversaciones amistosas, y necesitamos tu ayuda para [mantener las cosas civilizadas](%{guidelines_url}). Si ves un problema, por favor marca con la bandera y de forma privada se le notificará al autor o bien se notificará al [staff](%{about_url}).
> :imp: Escribí algo desagradable aquí
Supongo que sabes qué hacer. Sigue adelante y **reporta este mensaje** <img src="/images/font-awesome-flag.png" width="16" height="16"> como inapropiado!
reply: |-
[Nuestro staff](/groups/staff) será notificado por privado sobre tu reporte. Si un número suficiente de miembros reportan un mensaje, será ocultado automáticamente como precaución. (Puesto que no escribí algo realmente desagradable :angel:, he quitado el reporte por ahora.)
not_found: |-
Oh no, mi mensaje desagradable aún no ha sido reportado! :worried: ¿Puedes marcar como inapropiado el mensaje con la **bandera** <img src="/images/font-awesome-flag.png" width="16" height="16">? No te olvides de hacer clic en el botón para mostrar todas las opciones: <img src="/images/font-awesome-ellipsis.png" width="16" height="16"> y ver así más acciones por cada mensaje.
search:
instructions: |-
_psst_ … Te he ocultado una sorpresa en este tema. Si aceptas el reto, **selecciona el icono de búsqueda** <img src="/images/font-awesome-search.png" width="16" height="16"> en la parte superior &#8599; y busca lo siguiente.
Prueba buscar el término "capy&#8203;bara" en este tema
hidden_message: |-
Cómo te perdiste este capybara? :wink:
<img src="/images/capybara-eating.gif"/>
¿Te fijaste en que estás de vuelta al principio? Alimenta este pobre hambriendo capybara **respondiendo con el emoji `:herb:`** y serás automáticamente enviado al final.
reply: |-
Yay lo encontraste! :tada:
- Para búsquedas más detalladas, ir a la página de [busqueda completa](%{search_url}).
- Para saltar donde quieras en una discusión larga, intente los controles de la línea de tiempo del tema a la derecha (o por debajo, en el móvil)
- Si tienes un :keyboard: físico, presiona la tecla <kbd>?</kbd> para ver nuestros prácticos atajos del teclado.
not_found: |-
Hmm… parece que podrías estar teniendo problemas. Lo siento acerca de ésto. Buscaste <img src="/images/font-awesome-search.png" width="16" height="16"> el término **capy&#8203;bara**?
end:
message: |-
Gracias por quedarte conmigo @%{username}! Hice esto por tí, creo que te lo has ganado:
%{certificate}
¡Eso es todo por ahora! Echa un vistazo a [**nuestros últimos temas de discusión**](/latest) o las [**categorías de debate**](/categories). :sunglasses:
(Si deseas hablar conmigo de neuvo para aprender más, solo envia un mensaje o me mencionas `@%{discobot_username}` en cualquier momento!)
certificate:
alt: 'Certificado de logro'
advanced_user_narrative:
reset_trigger: 'usuario avanzado'
cert_title: "En reconocimiento de la finalización exitosa del tutorial de usuario avanzado"
title: ':arrow_up: Funciones avanzadas del usuario'
start_message: |-
Como un usuario _avanzado_, ¿no has visitado [tus preferencias](/my/preferences) aún @%{username}? Hay muchas formas de personalizar tu experiencia, por ejemplo podrías elegir un diseño oscuro.
¡Pero yo divago, comencemos!
edit:
bot_created_post_raw: "@%{discobot_username} es, por lejos, el mejor bot que conozco :wink:"
instructions: |-
Todos cometemos errores. Pero no te preocupes, siempre podrás editar tus mensajes para solucionarlos!
¿Puedes empezar por **editar** el mensaje que acabo de crear en tu nombre?
not_found: |-
Parece que aún no has editado el [mensaje](%{url}) que creé por tí. ¿Puedes intentar de nuevo?
Usa el <img src="/images/font-awesome-pencil.png" width="16" height="16"> icono de lápiz para que aparezca el editor.
reply: |-
¡Buen trabajo!
Tenga en cuenta que las ediciones realizadas después de 5 minutos aparecerán como revisiones de edición pública y aparecerá un pequeño ícono de lápiz en la parte superior derecha con el recuento de revisiones.
delete:
instructions: |-
Si deseas retirar una publicación que hayas realizado, puedes eliminarla.
Vamos a **borrar** cualquier post que hayas escrito usando la acción <img src="/images/font-awesome-trash.png" width="16" height="16"> para **borrar**. No vayas a querer borrar el primer post!
not_found: |-
¿Todavía no veo ninguna publicación eliminada? Recuerda presionar <img src="/images/font-awesome-ellipsis.png" width="16" height="16"> para mostrar el botón <img src="/images/font-awesome-trash.png" width="16" height="16"> y borrar.
reply: |-
Whoa! :boom:
Para preservar la continuidad de las discusiones, las supresiones no son inmediatas, por lo que la publicación se eliminará después de algún tiempo.
recover:
deleted_post_raw: 'Por qué @%{discobot_username} borró mi mensaje? :anguished:'
instructions: |-
Oh no! Parece que accidentalmente borré un nuevo mensaje que tú acababas de crear.
¿Puedes hacerme el favor y hacer clic en <img src="/images/font-awesome-rotate-left.png" width="16" height="16"> para **recuperar** el mensaje borrado?
not_found: |-
¿Teniendo problemas? Recuerda <img src="/images/font-awesome-ellipsis.png" width="16" height="16"> muestra más, y aparecerá el botón <img src="/images/font-awesome-rotate-left.png" width="16" height="16"> para recuperar.
reply: |-
Phew, ¡eso estuvo cerca! Gracias por solucionarlo :wink:
Tenga en cuenta que sólo tiene 24 horas para recuperar una publicación.
category_hashtag:
instructions: |-
¿Sabía que puede hacer referencia a categorías y etiquetas en su publicación? Por ejemplo, has visto la %{category} categoría?
Escribe `#` en el medio de una oración y selecciona cualquier categoría o etiqueta.
not_found: |-
Hmm, no veo una categoría en ninguna parte. Nota que `#` no puede ser el primer carácter de la oración. ¿Puedes copiar esto en tu próxima respuesta?
```text
Yo puedo crear un enlace a la categoría con #
```
reply: |-
¡Excelente! Recuerda que ésto funciona para ambos, categorías _y_ etiquetas, si las etiquetas están disponibles.
change_topic_notification_level:
instructions: |-
Cada tema tiene un nivel de notificación. Comienza en 'normal', lo que significa que normalmente sólo se notificará cuando alguien está hablando directamente con usted.
De forma predeterminada, el nivel de notificación de un mensaje privado se establece en el nivel más alto de "observación", lo que significa que se le notificará cada nueva respuesta. Pero puede sobrescribir el nivel de notificación de _cualquier_ tema a 'vigilar', 'seguir' o 'silenciar'.
Intentemos cambiar el nivel de notificación de este tema. Al final del tema, encontrarás un botón que muestra que estás **vigilando** este tema. ¿Puedes cambiar el nivel de notificación a **seguir**?
not_found: |-
Parece que aún estás vigilando :eyes: este tema! Si tienes problemas para encontrar el botón del nivel de notificación, el mismo está debajo de todo el tema de debate.
reply: |-
¡Impresionante trabajo! Espero que no silencies este tema ya que puedo ser un poco hablador a veces :grin:.
Tenga en cuenta que cuando responde a un tema o lee un tema durante más de unos minutos, se establece automáticamente en un nivel de notificación de "seguimiento". Puedes cambiar esto en [tus preferencias de usuario](/my/preferences).
poll:
instructions: |-
¿Sabes que puedes agregar una encuesta en cualquier mensaje? Intenta usando el botón de engranaje <img src="/images/font-awesome-gear.png" width="16" height="16"> en el editor para **armar una encuesta**.
not_found: |-
Whoops! No hubo ninguna encuesta en tu respuesta.
Usa el ícono de engranaje: <img src="/images/font-awesome-gear.png" width="16" height="16"> en el editor, o bien copia y pega esta encuesta en tu próxima respuesta:
```text
[poll]
* :cat:
* :dog:
[/poll]
```
reply: |-
Hey, ¡hermosa encuesta! ¿Cómo te enseñé?
[poll]
* :+1:
* :-1:
[/poll]
details:
instructions: |-
A veces, tú puedes decidir **ocultar detalles** en tus respuestas:
- Cuando estás discutiendo puntos de trama de una película o programa de televisión que sería considerado un spoiler.
- Cuando su puesto necesita un montón de detalles opcionales que pueden ser abrumador cuando se lee todo a la vez.
[details=¡Clic aquí para ver cómo funciona!]
1. Selecciona el botón de engranaje <img src="/images/font-awesome-gear.png" width="16" height="16"> en el editor.
2. Selecciona "Hide Details".
3. Edita los detalles, y agrega el contenido que desees ocultar.
[/details]
¿Puedes usar el botón de engranaje <img src="/images/font-awesome-gear.png" width="16" height="16"> en el editor para agregar datos ocultos en tu próxima respuesta?
not_found: |-
¿Tiene problemas para crear un widget de detalles? Trate de incluir lo siguiente en su siguiente respuesta:
```text
[details=Seleccionar para ver detalles]
Aquí están los detalles
[/details]
```
reply: |-
¡Gran trabajo — tu atención a los _detalles_ es admirable!
end:
message: |-
Usted ha brillado a través de esto como un _usuario avanzado_ de hecho :bow:
%{certificate}
Eso es todo lo que tengo para ti.
¡Adiós por ahora! Si desea hablar conmigo de nuevo, envíeme un mensaje en cualquier momento :sunglasses:
certificate:
alt: 'Advanced User Track Certificate of Achievement'

View File

@ -0,0 +1,453 @@
fi:
site_settings:
discourse_narrative_bot_enabled: 'Ota Discoursen opastava botti käyttöön'
disable_discourse_narrative_bot_welcome_post: "Estä bottia lähettämästä tervetuloviestejä"
discourse_narrative_bot_ignored_usernames: "Käyttäjänimet, jotka botin tulisi jättää huomiotta"
discourse_narrative_bot_disable_public_replies: "Estä bottia vastaamasta julkisesti ketjuihin"
discourse_narrative_bot_welcome_post_type: "Minkätyyppisen tervetuloviestin botti lähettää"
badges:
certified:
name: Tutkinto plakkarissa
description: "Suoritti peruskurssin"
long_description: |
Tämä ansiomerkki myönnetään, kun suoritat menestyksellä vuorovaikutteisen palstan käytön peruskurssin. Otit tavoitteeksesi hallita tavallisimmat toiminnot, ja nyt sinulla on siitä todistus!
licensed:
name: Pätevyytensä osoittanut
description: "Suoritti jatkokurssin"
long_description: |
Tämä ansiomerkki myönnetään, kun suoritat menestyksellä vuorovaikutteisen palstan käytön jatkokurssin. Olet omaksunut edistyneemmätkin toiminnot, ja olet taitosi osoittanut.
discourse_narrative_bot:
bio: "Moi! En ole ihminen. Olen botti, ja tarjoan opetusta sivuston käyttämisestä. Jos haluat jutella, lähetä yksityisviesti tai mainitse **`@%{discobot_username}`** missä vain."
timeout:
message: |-
Hei @%{username}, kyselen kuulumisia, kun en ole kuullut sinusta hetkeen.
- Voit jatkaa milloin vain vastaamalla minulle.
- Voit hypätä tämän vaiheen yli sanomalla `@%{discobot_username} %{skip_trigger}`.
- Voit aloittaa alusta sanomalla `@%{discobot_username} %{reset_trigger}`.
Jos et viitsi, sekin on ihan okei. Olen robotti. Minulla ei ole tunteita. :sob:
dice:
trigger: "heitä"
invalid: |-
Pahoittelen, mutta on matemaattinen mahdottomuus heittää noppia sillä tavalla kuin pyysit. :confounded:
not_enough_dice: |-
Minulla on vain %{num_of_dice} noppaa. [Häpeällistä](http://www.therobotsvoice.com/2009/04/the_10_most_shameful_rpg_dice.php), tiedän!
out_of_range: |-
Tiesitkö, että [suurin mahdollinen määrä sivuja](https://www.wired.com/2016/05/mathematical-challenge-of-designing-the-worlds-most-complex-120-sided-dice) matemaattisesti tasapuolisessa nopassa on 120?
results: |-
> :game_die: %{results}
quote:
trigger: "sitaatti"
"1":
quote: "Tutki parhaita kirjailijoita siten avartaaksesi mielikuvituksesi piiriä."
author: "Aleksis Kivi"
"2":
quote: "On ihan mahdollista, että todellisuus on olemassa."
author: "Esko Valtaoja"
"3":
quote: "Jokainen syy, joka estää kuntoilun, on tekosyy."
author: "Urho Kekkonen"
"4":
quote: "Älkää kiinnittäkö huomiota mitä kriitikot sanovat, kriitikoille ei ole koskaan pystytetty yhtään patsasta."
author: "Jean Sibelius"
"5":
quote: "Kahdesta vaihtoehdosta koetan valita aina sen, joka pelottaa enemmän."
author: "Jouko Turkka"
"6":
quote: "Jos Suomessa juodaankin jokunen Koskenkorva liikaa, on siihen hyvä syy: paljon muuta tekemistä ei löydy."
author: Linus Torvalds
"7":
quote: "Kaikki lähtee siitä kun sä opit syömään lihapullat haarukalla. Silloin sä osaat tehdä mitä tahansa."
author: "Matti Nykänen"
"8":
quote: "Käsitykseni on, että se, mitä Suomen kansa tällä hetkellä ennen kaikkea kaipaa, on läheistä, asiallisuuteen pohjautuvaa ymmärtämystä eri yhteiskuntalukkien, eri kieliryhmien ja eri puolueiden kesken. Se takaa jatkuvan yhteiskunnallisen rauhan ja siitä riippuu, missä määrin tämä kansa kykenee vapauttansa ja itsenäisyytensä lujittamaan ja tarvittaessa puolustamaan."
author: "Lauri Kristian Relander"
"9":
quote: "Naisen työala tulevaisuudessa on laaja, hänen tehtävänsä tärkeä. Mitä vuosisadat, vuosituhannet ovat rikkoneet ja laiminlyöneet, se kaikki tulee hänen korjata ja parantaa."
author: "Minna Canth"
"10":
quote: "Joskus pitää laittaa viatonkin vankilaan. Joskus täytyy poliisin laittaa viaton lusimaan. Arvovalta säilyy. Pelote säilyy. Aina siellä muutama viaton istuu, viaton pulmunen istuu siellä."
author: "Rauno Repomies televisiosarjassa Pasila"
results: |-
> :left_speech_bubble: _%{quote}_ &mdash; %{author}
magic_8_ball:
trigger: 'ennustus'
answers:
"1": "Se on varmaakin varmempaa"
"2": "Niin on tapahtuva"
"3": "Epäilemättä"
"4": "Kyllä, ehdottomasti"
"5": "Voit laskea sen varaan"
"6": "Näkemykseni on, että kyllä"
"7": "Todennäköisesti"
"8": "Siltä näyttäisi"
"9": "Kyllä"
"10": "Merkit viittaavat siihen suuntaan"
"11": "Näky epäselvä, yritä uudelleen"
"12": "Palaa myöhemmin asiaan"
"13": "Parempi, etten kerro vielä"
"14": "En pysty sanomaan juuri nyt"
"15": "Keskity ja kyse sitten uudelleen"
"16": "Älä laske sen varaan"
"17": "Vastaukseni on ei"
"18": "Lähteeni kertovat, että ei"
"19": "Ei siltä vaikuta"
"20": "Epäilen sitä vahvasti"
result: |-
> :crystal_ball: %{result}
track_selector:
random_mention:
reset_trigger: 'aloita'
skip_trigger: 'ohita'
help_trigger: 'apua'
reply: |-
Moi! Jos haluat tietää mitä osaan, sano `@%{discobot_username} %{help_trigger}`.
tracks: |-
Osaan nykyisellään seuraavat jutut:
`@%{discobot_username} %{reset_trigger} %{default_track}`
> Aloittaa yhden seuraavista opastetuista kierroksista: %{tracks}.
bot_actions: |-
`@%{discobot_username} %{dice_trigger} 2d6`
> :game_die: dice roll: 3, 6
`@%{discobot_username} %{quote_trigger}`
> :left_speech_bubble: _Carry out a random act of kindness, with no expectation of reward, safe in the knowledge that one day someone might do the same for you_ &mdash; Prinsessa Diana
`@%{discobot_username} %{magic_8_ball_trigger}`
> :crystal_ball: En pysty sanomaan juuri nyt
do_not_understand:
first_response: |-
Hei, kiitos kun vastasit!
Valitettavasti olen huonosti ohjelmoitu botti, enkä aivan ymmärtänyt mitä tarkoitit. :frowning:
track_response:
Voit yrittää uudelleen, tai jos mieluummin hyppäät vaiheen yli, sano `@%{discobot_username} %{skip_trigger}`. Tai jos haluat aloittaa alusta, sano `@%{discobot_username} %{reset_trigger}`.
second_response: |-
Voi ei, en vieläkään tajua. :anguished:
Olen kuitenkin vain botti. Jos haluat tavoittaa aidon ihmisen, katsopa [yhteystietoja tältä sivulta](/about).
En häiritse sinua enempää nyt.
new_user_narrative:
reset_trigger: "peruskurssi"
cert_title: "Tunnustuksena palstan käytön peruskurssin menestyksekkäästä suorittamisesta"
hello:
title: ":robot: Tervehdys!"
message: |-
Kiitos kun liityit sivustollemme %{title}. Tervetuloa!
- Olen vain robotti, mutta [avulias henkilökunta](/about) auttaa myös mielellään, jos sinulla on asiaa heille.
- Turvallisuussyistä uusi käyttäjä ei voi tehdä ihan kaikkea. Saat uusia toimintoja käyttöösi (ja [ansiomerkkejä](/badges)) kun tutustumme sinuun.
- Vannomme [sivistyneen yhteisökäyttäytymisen](/guidelines) nimeen kaikissa tilanteissa.
onebox:
instructions: |-
Voisitko nyt jakaa jonkun seuraavista linkeistä minulle? Laita vastaukseesi **linkin osoite yksin omalle rivilleen**, jolloin siitä muotoillaan automaattisesti kätevä tiivistelmälaatikko.
Kopioi linkki mobiililaitteella koskettamalla sitä hetken aikaa, tai klikkaa sitä hiiresi oikealla painikkeella:
- https://en.wikipedia.org/wiki/Inherently_funny_word
- https://en.wikipedia.org/wiki/Death_by_coconut
- https://en.wikipedia.org/wiki/Calculator_spelling
reply: |-
Siistiä! Suurin osa <img src="/images/font-awesome-link.png" width="16" height="16"> linkeistä käyttäytyy samoin. Muista, että sen tulee olla yksin omalla rivillään; mitään ei saa olla rivillä ennen sitä tai sen jälkeen.
not_found: |-
Voi ei, en löytänyt linkkiä vastauksestasi! :cry:
Kokeilepa lisätä tämä linkki seuraavaan vastaukseesi, omalle rivilleen.
- https://fi.wikipedia.org/wiki/Eksoottinen_lyhytkarva
images:
instructions: |-
Tämä on kuva yksisarvisesta:
<img src="/images/unicorn.jpg" width="520" height="381">
Jos pidit siitä (kukapa ei pitäisi!), kerro siitä minulle painamalla tykkää-nappia :heart: tämän viestin alla.
Osaatkohan **vastata kuvalla?** Ihan mikä tahansa kuva kelpaa! Raahaa ja pudota, paina liitä-nappia tai vaikkapa kopioi ja liitä. Keinot ovat monet!
reply: |-
Loistava kuva Painoin tykkää-nappia :heart:, jotta tiedät että pidin siitä kovasti! :heart_eyes:
like_not_found: |-
Unohditko tykätä :heart: [viestistäni?](%{url}) :crying_cat_face:
not_found: |-
Näyttää ettet laittanut kuvaa, joten valitsin kuvan josta _takuulla_ pidät.
`%{image_url}`
Kokeilepa liittää tämä kuva tai liittää linkin osoite omalle rivilleen!
formatting:
instructions: |-
**Lihavoi** tai **kursivoi** sanoja seuraavasta vastauksestasi.
- kirjoita `**lihavoitua**` tai `_kursivoitua_`
- tai käytä viestieditorin <kbd><b>B</b></kbd>- tai <kbd><i>I</i></kbd>-painiketta
reply: |-
Hyvin tehty! HTML ja BBCode käyvät myös muotoiluun. Jos on puhtia, [tästä johdannosta](http://commonmark.org/help) opit lisää. :nerd:
not_found: |-
Pahus, en näe muotoilua viestissäsi. :pencil2:
Kokeilisitko uudelleen? Käytä editorin <kbd><b>B</b></kbd>-lihavointipainiketta tai <kbd><i>I</i></kbd>-kursivointipainiketta, jos muuten ei meinaa onnistua.
quoting:
instructions: |-
Voisitko lainata minua kun vastaat, jotta tiedän mihin kohtaan viittaat?
> Jos tässä on kahvia, tuo minulle teetä. Jos tässä on teetä, tuo minulle kahvia.
>
> Hyvä puoli itsekseen juttelussa on, että voit olla varma, että joku kuuntelee.
>
> Jotkut ovat briljantteja sanankäyttäjiä ja toiset... öö... tuota... eivät ole.
Maalaa valitsemasi sitaatin &uarr; teksti ja paina sitten Lainaa-painiketta, joka ilmestyy valintasi yhteyteen. Lainaa-napin sijasta voit myös painaa Vastaa-nappia tämän viestin alla.
Kuvaile parilla sanalla sitaatin perään, miksi valitsit juuri sen, sillä olen kovin utelias. :thinking:
reply: |-
Satuit valitsemaan suosikkisitaattini! :left_speech_bubble:
not_found: |-
Hmm, minusta tuntuu ettet lainannut tekstiäni vastaukseesi?
<kbd>**Lainaa**</kbd>-painike ilmestyy, kun maalaat viestini tekstiä. **Vastaa**-painike käy yhtä hyvin, jos sinulla on tekstiä maalattuna. Kokeilisitko uudelleen?
bookmark:
instructions: |-
Jos haluat oppia lisää, valitse <img src="/images/font-awesome-ellipsis.png" width="16" height="16"> tämän alta ja <img src="/images/font-awesome-bookmark.png" width="16" height="16"> **lisää tämä yksityisviestiketju kirjanmerkkeihisi**. Jos teet niin, sinua voi odottaa :gift: tulevaisuudessa!
reply: |-
Erinomaista! Löydät yksityiskeskustelumme nyt helposti koska vain [profiilisi kirjanmerkkivälilehdeltä](%{profile_page_url}/activity/bookmarks). Klikkaat vain profiilikuvaasi oikeassa yläkulmasssa &#8599;
not_found: |-
Tuota noin, et ole mielestäni kirjanmerkinnyt tämän ketjun viestejä. Löysithän kirjanmerkkikuvakkeen kunkin viestin alta? Voi olla, että sinun täytyy klikata <img src="/images/font-awesome-ellipsis.png" width="16" height="16">-kuvaketta, jotta näet sen ja muita toimintoja.
emoji:
instructions: |-
Tapaan käyttää pieniä kuvia :blue_car::dash: viesteissäni; ne ovat [emojeja](https://fi.wikipedia.org/wiki/Emoji). **Laitapa joku emoji** vastausviestiisi. On useita tapoja:
- Näppäile `:) ;) :D :P :O`
- Laita kaksoispiste <kbd>:</kbd> ja sen perään emojin nimi `:tada:`
- Paina editorin tai mobiililaitteesi näppäimistön emoji-kuvaketta <img src="/images/font-awesome-smile.png" width="16" height="16">.
reply: |-
:sparkles: _Emojinomaista!_ :sparkles:
not_found: |-
Oho, et tainnut laittaa emojia viestiisi. Nyyh! :sob:
Saat emojivalitsimen esiin näppäilemällä kaksoispisteen <kbd>:</kbd>. Ala kirjoittaa sen perään englanniksi millaisen emojin haluat - esimerkiksi lintu on `:bird:`
Voit myös painaa editorin emojikuvaketta <img src="/images/font-awesome-smile.png" width="16" height="16">.
(Mobiililaitteella voit lisätä emojin suoraan näppäimistöltäsikin.)
mention:
instructions: |-
Voit haluta huomiota joltakulta, vaikket edes olisi vastaamassa hänelle suoraan. Mainitse hänet näppäilemällä `@` ja sen perään käyttäjänimi kokonaisuudessaan.
Mainitsepa minut **`@%{discobot_username}`** seuraavassa vastauksessasi.
reply: |-
_Kutsuiko joku minua!?_ :raised_hand: Sinäkö se olit? :wave: Tässäpä minä! Kiitos kun mainitsit minut. :ok_hand:
not_found: |-
En näe nimeäni. :frowning: Yrittäisitkö nimeni `@%{discobot_username}` mainitsemista vielä kerran?
(Huomaa, että nimessäni on kansainvälisessä hengessä _disco_, ihan kuin siinä 1970-luvun tanssityylissä. [Rakastan yöelämää!](https://www.youtube.com/watch?v=B_wGI3_sGf8) :dancer:)
flag:
instructions: |-
Pidämme ystävällishenkisistä keskusteluista, ja tarvitsemme apuasi, jotta [pysytään asialinjalla](%{guidelines_url}). Kun näet ongelman, kerro siitä kirjoittajalle tai [avuliaalle henkilökunnalle](%{about_url}) liputtamalla.
> :imp: Kirjoitin tähän jotain tuhmaa.
Tiedät mitä tehdä. **Liputa tämä viesti** <img src="/images/font-awesome-flag.png" width="16" height="16"> sopimattomaksi!
reply: |-
[Henkilökuntamme](/groups/staff) saa ei-julkisen ilmoituksen lipusta. Jos riittävän moni yhteisön jäsen liputtaa viestin, sekin riittää viestin automaattiseen piilottamiseen varotoimena. (Koska en oikeasti kirjoittanut mitään tuhmaa :angel:, menin ja poistin liputuksesi.)
not_found: |-
Oi voi, tuhmaa viestiäni ei ole vielä liputettu. :worried: Voitko liputtaa sen sopimattomaksi **lippupainikkeen** avulla <img src="/images/font-awesome-flag.png" width="16" height="16">? Dont forget to use the show more button <img src="/images/font-awesome-ellipsis.png" width="16" height="16"> to reveal more actions for each post.
search:
instructions: |-
_psst_ … Tein pienen jekun tähän ketjuun. Jos olet valmis ottamaan haasteen vastaan, oikealla ylhäällä &#8599; on **hakukuvake** <img src="/images/font-awesome-search.png" width="16" height="16">, jolla voit yrittää löytää sen.
Etsi hakusanaa "kapy&#8203;bara" tästä ketjusta
hidden_message: |-
Miten sinulta jäi tämä kapybara huomaamatta? :wink:
<img src="/images/capybara-eating.gif"/>
Huomasitko, että palasit takaisin alkuun? Ruoki söpöä nälkäistä kapybaraa **vastaamalla `:herb:` -emojilla** niin sinut palautetaan automaattisesti takaisin loppuun.
reply: |-
Jes, löysit sen :tada:
- Tarkempia hakuja voit tehdä [suurella hakusivulla](%{search_url}).
- Hyppää mihin kohtaan haluat pitkää keskustelua oikealla olevalla aikajanan avulla (sijaitsee mobiilissa alhaalla).
- Jos sinulla on fyysinen :keyboard:, tarkastele käteviä näppäinoikoteitä painamalla <kbd>?</kbd>.
not_found: |-
Hm… sinulla vaikuttaa olevan vaikeuksia. Pahoitteluni. Haitko <img src="/images/font-awesome-search.png" width="16" height="16"> hakusanalla **kapy&#8203;bara**?
end:
message: |-
Kiitos kun jaksoit loppuun asti @%{username}! Tein tämän sinulle, minusta olet ansainnut sen:
%{certificate}
Siinä kaikki tältä erää! Tsekkaa [**tuoreimmat ketjut**](/latest) tai tutki [**keskustelualueita**](/categories). :sunglasses:
(Jos haluat vielä jutella kanssani ja oppia lisää, lähetä minulle `@%{discobot_username}` yksityisviesti tai mainitse minut koska vain!)
certificate:
alt: 'Diplomi'
advanced_user_narrative:
reset_trigger: 'jatkokurssi'
cert_title: "Tunnustuksena palstan käytön jatkokurssin menestyksekkäästä suorittamisesta"
title: ':arrow_up: Jatkokurssi'
start_message: |-
_Edistynyt_ käyttäjä kun olet, oletko @%{username} jo käynyt [käyttäasetussivulla](/my/preferences)? Siellä voit mukauttaa sivustoa makusi mukaan monilla tavoin, esimerkiksi valita tumman ja vaalean teeman väliltä.
Vaan eipä harhauduta asiasta vaan aloitetaan!
edit:
bot_created_post_raw: "@%{discobot_username} on ylivoimaisesti mahtavin botti, jonka tiedän. :wink:"
instructions: |-
Kaikki tekevät virheitä. Vaan älä huoli, voit muokata viestejäsi ja korjata erheesi!
Aloita **muokkaamalla** viestiä, jonka juuri tein sinun nimissäsi.
not_found: |-
Näyttää, ettet vielä muokannut [viestiä](%{url}), jonka tein puolestasi. Yrittäisitkö uudelleen?
Palaa viestieditoriin klikkaamalla <img src="/images/font-awesome-pencil.png" width="16" height="16">-kuvaketta.
reply: |-
Hyvin tehty!
Huomaa, että 5 minuutin jälkeen tehdyt muokkaukset näkyvät kaikille muokkaushistoriassa, joka ilmestyy viestin oikeaan yläkulmaan pienenä kynäsymbolina, jossa näkyy muokkausten määrä.
delete:
instructions: |-
Jos haluat perua viestisi, voit poistaa sen.
Anna mennä ja **poista** yltä mikä tahansa viestisi <img src="/images/font-awesome-trash.png" width="16" height="16"> poista-toiminnon avulla. Älä kuitenkaan erehdy poistamaan ketjun ensimmäistä viestiä!
not_found: |-
Minusta viestejä ei vielä poistettu? Muista, että <img src="/images/font-awesome-ellipsis.png" width="16" height="16"> näytä lisää -kuvake paljastaa <img src="/images/font-awesome-trash.png" width="16" height="16"> poista-kuvakkeen.
reply: |-
Vau! :boom:
Keskusteluiden jatkuvuuden vuoksi viesti ei poistu kokonaan välittömästi, vaan se poistuu vasta jonkin ajan kuluttua.
recover:
deleted_post_raw: 'Miksi @%{discobot_username} poisti viestini? :anguished:'
instructions: |-
Voi ei! Taisin vahingossa poistaa viestin, jonka tein sinun nimissäsi.
Tekisitkö palveluksen ja <img src="/images/font-awesome-rotate-left.png" width="16" height="16"> **palauttaisit** sen?
not_found: |-
Onko ongelmia? Muista, että <img src="/images/font-awesome-ellipsis.png" width="16" height="16"> näytä lisää -kuvake paljastaa <img src="/images/font-awesome-rotate-left.png" width="16" height="16"> palauta-kuvakkeen.
reply: |-
Huh, sydäntä kylmäsi! Kiitos, kun korjasit mokani. :wink:
Huomioi, että viestin voi palauttaa vain 24 tunnin sisällä sen poistamisesta.
category_hashtag:
instructions: |-
Tiesitkö, että viesteissä voi viitata alueisiin ja tunnisteisiin? Esimerkiksi, oletko käynyt %{category}-alueella?
Näppäile lauseen keskellä `#` ja valitse mieleinen alue tai tunniste.
not_found: |-
Höh, en näe viittauksia alueisiin. Huomioi, ettei `#` saa olla rivinsä ensimmäinen merkki. Kopioisitko tämän seuraavaan vastaukseesi?
```text
I can create a category link via #
```
reply: |-
Erinomaista! Muista, että tämä toimii alueiden lisäksi myös tunnisteille, jos ne ovat käytössä täällä.
change_topic_notification_level:
instructions: |-
Sinulla on jokaiselle ketjulle ilmoitustaso. Aluksi se on 'tavallinen', jolloin saat tavalliseen tapaan ilmoituksen vain, jos joku puhuu nimenomaan sinulle.
Oletuksena yksityisviestiketjujen ilmoitustaso on kaikkein korkein eli 'tarkkaillaan', jolloin saat ilmoituksen joka ikisestä viestistä. Voit kuitenkin valita _mille tahansa_ ketjulle ilmoitustason 'tarkkaillaan', 'seurataan' tai 'vaimennettu'.
Kokeillaanpa vaihtaa tämän ketjun ilmoitustasoa. Ketjun alla on painike, joka näyttää sinun tarkkailevan tätä ketjua. Vaihdapa ilmoitustasoksi **seurataan**.
not_found: |-
Näytät yhä tarkkailevan :eyes: tätä ketjua! Jollet meinaa löytää sitä, ilmoitustasopainike sijaitsee ketjun alla.
reply: |-
Mahtavaa! Toivottavasti et kuitenkaan vaimentanut tätä ketjua, vaikka saatan välillä olla vähän liiankin puhelias. :grin:.
Huomioi, että kun vastaat ketjuun tai luet sitä kauemmin kuin muutaman minuutin ajan, se saa automaattisesti ilmoitustasokseen 'seurataan'. Tätä voi säätää [käyttäjäasetuksissa](/my/preferences).
poll:
instructions: |-
Tiesitkö, että viesteihin voi lisätä äänestyksiä? **Luo äänestys** valitsemalla viestieditorin <img src="/images/font-awesome-gear.png" width="16" height="16"> hammasrataskuvake.
not_found: |-
Ups! Vastauksessasi ei ollut äänestystä.
Käytä editorin <img src="/images/font-awesome-gear.png" width="16" height="16"> hammasrataskuvaketta tai kopioi tämä äänestys ja liitä se seuraavaan vastaukseesi:
```text
[poll]
* :cat:
* :dog:
[/poll]
```
reply: |-
Hauska äänestys! Miten onnistuin opetuksessa?
[poll]
* :+1:
* :-1:
[/poll]
details:
instructions: |-
Joskus voit haluta **piilottaa yksityiskohtia** vastauksissasi:
- Kirjoitat elokuvasta tai TV-ohjelmasta jotakin, minkä voi kokea juonipaljastukseksi.
- Viestissäsi on suuria määriä yksityiskohtaista tietoa, joka ei ole kaikille olennaista ja joka voisi uuvuttaa tai häiritä lukijaa.
[details=Select this to see how it works!]
1. Valitse editorin <img src="/images/font-awesome-gear.png" width="16" height="16"> hammasrataskuvake.
2. Valitse "Piilota yksityiskohdat".
3. Muokkaa Yhteenveto-tekstiä, jonka taakse tiedot piilotetaan, ja lisää sisältö.
[/details]
Piilottaisitko yksityiskohtia editorin <img src="/images/font-awesome-gear.png" width="16" height="16"> hammasrataskuvakkeen avulla seuraavasta vastauksestasi?
not_found: |-
Onko ongelmia yksityiskohtatoiminnon kanssa? Kokeilepa laittaa tämä seuraavaan vastaukseesi:
```text
[details=Paina niin saat tarkempaa tietoa]
Yksityiskohtaista tietoa
[/details]
```
reply: |-
Hyvin tehty - panostustasi _yksityiskohtiin_ on pakko arvostaa!
end:
message: |-
Selvitit tämän kuin _edistynyt käyttäjä_ konsanaan! :bow:
%{certificate}
Siinä oli kaikki, mitä minulla on tarjota.
Kuulemiin! Jos haluat vielä jutella kanssani, lähetä viesti milloin vain! :sunglasses:
certificate:
alt: 'Diplomi jatkokurssista'

View File

@ -0,0 +1,410 @@
it:
site_settings:
discourse_narrative_bot_enabled: 'Abilita Discourse Narrative Bot'
disable_discourse_narrative_bot_welcome_post: "Disabilita il messaggio di benvenuto di Discourse Narrative Bot"
discourse_narrative_bot_ignored_usernames: "Gli username che Discourse Narrative Bot dovrebbe ignorare"
discourse_narrative_bot_disable_public_replies: "Disabilita le risposte pubbliche da Discourse Narrative Bot"
badges:
certified:
name: Certificato
description: "Completato il tutorial per nuovi utenti"
long_description: |
Questo distintivo è assegnato al completamento del tutorial interattivo per nuovi utenti. Hai voluto imparare gli strumenti base di una discussione, e ora sei certificato!
licensed:
name: Diploma
description: "Completato il tutorial per utenti avanzati"
long_description: |
Questo distintivo è assegnato al completamento del tutorial interattivo per utenti avanzati. Hai imparato gli strumenti avanzati di discussione — e ora sei un diplomato!
discourse_narrative_bot:
bio: "Ciao, io non sono una persona reale. Io sono un bot che ti può insegnare ad usare questo sito. Per interagire con me inviami un messaggio o menziona**`@%{discobot_username}`** ovunque."
timeout:
message: |-
Hey @%{username}, sto solo controllando perchè è da un po' che non ti sento.
- Per continuare, rispondimi in qualsiasi momento.
- Se vuoi saltare questo passaggio, dimmi `%{skip_trigger}`.
- Per ricominciare, dimmi `%{reset_trigger}`.
Se preferisci non continuare, va bene lo stesso. Io sono un robot. Non ferirai i miei sentimenti. :sob:
dice:
trigger: "tira"
invalid: |-
Mi dispiace, è matematicamente impossibile lanciare quella combinazione di dadi. :confounded:
not_enough_dice: |-
Ho solo %{num_of_dice} dadi. [Vergognoso](http://www.therobotsvoice.com/2009/04/the_10_most_shameful_rpg_dice.php), lo so!
out_of_range: |-
Lo sapevi che [il numero massimo di lati](https://www.wired.com/2016/05/mathematical-challenge-of-designing-the-worlds-most-complex-120-sided-dice) per essere matematicamente bilanciato è 120?
results: |-
> :game_die: %{results}
quote:
trigger: "cita"
"1":
quote: "Nel mezzo delle difficoltà nascono le opportunità."
author: "Albert Einstein"
"2":
quote: "Siate il cambiamento che volete vedere nel mondo."
author: "Mahatma Gandhi"
"3":
quote: "Non piangere perchè è finita, sorridi perchè è successo."
author: "Dr Seuss"
"4":
quote: "Se vuoi fare bene una cosa, falla da solo."
author: "Charles-Guillaume Étienne"
"5":
quote: "Se credi che sia possibile sei già a metà strada."
author: "Theodore Roosevelt"
"6":
quote: "La vita è come una scatola di cioccolatini. Non sai mai quello che ti capita."
author: "Madre di Forrest Gump"
"7":
quote: "E' un piccolo passo per l'uomo ma un grande passo per l'umanità."
author: "Neil Armstrong"
"8":
quote: "Almeno una volta al giorno fai qualcosa che ti spaventa."
author: "Eleanor Roosevelt"
"9":
quote: "Gli errori sono sempre perdonabili, se si ha il coraggio di ammetterli."
author: "Bruce Lee"
"10":
quote: "Tutto ciò che la mente umana può concepire e credere, può essere realizzato."
author: "Napoleon Hill"
results: |-
> :left_speech_bubble: _%{quote}_ &mdash; %{author}
magic_8_ball:
trigger: 'fortuna'
answers:
"1": "E' certo"
"2": "E' decisamente così"
"3": "Senza alcun dubbio"
"4": "Sì, senza dubbio"
"5": "Ci puoi contare"
"6": "Per quanto posso vedere, sì"
"7": "Molto probabilmente"
"8": "Le prospettive sono buone"
"9": "Sì"
"10": "I segni indicano di sì"
"11": "E' difficile rispondere, prova di nuovo"
"12": "Rifai la domanda più tardi"
"13": "Meglio non risponderti adesso"
"14": "Non posso predirlo ora"
"15": "Concentrati e rifai la domanda"
"16": "Non ci contare"
"17": "La mia risposta è no"
"18": "Le mie fonti dicono di no"
"19": "Le prospettive non sono buone"
"20": "Molto incerto"
result: |-
> :crystal_ball: %{result}
track_selector:
reset_trigger: 'inizia'
skip_trigger: 'salta'
help_trigger: 'mostra la guida'
random_mention:
reply: |-
Ciao! Per sapere cosa posso fare, dimmi `@%{discobot_username} %{help_trigger}`.
tracks: |-
Attualmente so fare le seguenti cose:
`@%{discobot_username} %{reset_trigger} %{default_track}`
> Avvia una delle seguenti narrazioni interattive: %{tracks}.
bot_actions: |-
`@%{discobot_username} %{dice_trigger} 2d6`
> :game_die: 3, 6
`@%{discobot_username} %{quote_trigger}`
> :left_speech_bubble: _Fare un atto di gentilezza casuale, senza nessuna aspettativa di ricompensa, con la certezza che un giorno qualcuno potrebbe fare lo stesso per te_ &mdash; Principessa Diana
`@%{discobot_username} %{magic_8_ball_trigger}`
> :crystal_ball: Ci puoi contare
do_not_understand:
first_response: |-
Hey, grazie per la risposta!
Sfortunatamente, come un bot mal programmato, non posso capire la tua risposta. :frowning:
track_response:
Puoi provare di nuovo, o se vuoi saltare questo passaggio, dimmi `%{skip_trigger}`. Altrimenti per ricominciare, dimmi `%{reset_trigger}`.
second_response: |-
Aw, scusa. Non sto ancora capendo. :anguished:
Sono solo un bot, ma se vuoi contattare una persona reale, guarda [la nostra pagina dei contatti](/about).
Nel frattempo io me ne starò in disparte.
new_user_narrative:
reset_trigger: "nuovo utente"
cert_title: "In riconoscimento del successo nel completamento del tutorial come nuovo utente"
hello:
title: ":robot: Saluti!"
message: |-
Grazie per esserti unito a %{title}, e benvenuto!
- Io sono solo un robot, ma [il nostro amichevole staff](/about) è qui per aiutarti se hai bisogno di contattare una persona.
- Per ragioni di sicurezza, abbiamo temporaneamente limitato ciò che i nuovi utenti possono fare. Potrai guadagnare nuove abilità (e [distintivi](/badges)) appena ti conosceremo meglio.
- Noi crediamo da sempre in una [comunità dal comportamento civile](/guidelines).
onebox:
instructions: |-
Adesso, puoi condividere uno di questi collegamenti con me? Rispondi con **un collegamento su una riga a sè stante**, e verrà automaticamente espanso per includere un bel sommario.
Per copiare un collegamento, toccalo e tienilo premuto su mobile, o fai click col tasto destro del mouse:
- https://it.wikipedia.org/wiki/Italia
- https://it.wikipedia.org/wiki/Pagina_principale
- https://it.wikipedia.org/wiki/Traduzione
reply: |-
Fantastico! Questo funzionerà per la maggior parte dei collegamenti <img src="/images/font-awesome-link.png" width="16" height="16">. Ricorda, deve essere su una linea _da solo_, con nient'altro davanti o dietro.
not_found: |-
Mi dispiace, non riesco a trovare il collegamento nella tua risposta! :cry:
Puoi provare ad aggiungere il seguente collegamento, su una riga da solo, nella tua prossima risposta?
- https://it.wikipedia.org/wiki/Exotic_Shorthair
images:
instructions: |-
Ecco l'immagine di un unicorno:
<img src="/images/unicorn.jpg" width="520" height="381">
Se ti piace (e a chi non piacerebbe!) vai avanti e premi il pulsante mi piace :heart: sotto questo messaggio per farmelo sapere.
Puoi **rispondere con un'immagine?** Qualsiasi immagine funzionerà! Trascinala e rilasciala, premi il pulsante carica, oppure copiala e incollala.
reply: |-
Bell'immagine Ho premuto il pulsante mi piace :heart: per farti sapere quanto l'ho apprezzata :heart_eyes:
like_not_found: |-
Hai dimenticato di mettere mi piace :heart: al mio [messaggio?](%{url}) :crying_cat_face:
not_found: |-
Sembra che tu non abbia caricato un'immagine così ne ho scelta una io che sono _sicuro_ ti piacerà.
`%{image_url}`
Prova a caricare questa nel prossimo messaggio, o incolla il collegamento su una riga da solo!
formatting:
instructions: |-
Puoi scrivere alcune parole in **grassetto** o _italico_ nella tua risposta?
- digita `**grassetto**` o `_italico_`
- oppure premi i pulsanti <kbd><b>G</b></kbd> o <kbd><i>I</i></kbd> sull'editor
reply: |-
Ottimo lavoro! Anche HTML e BBCode funzionano per la formattazione per saperne di più [prova questo tutorial](http://commonmark.org/help) :nerd:
not_found: |-
Aww, non ho trovato nessuna formattazione nella tua risposta. :pencil2:
Puoi provare di nuovo? Usa i pulsanti <kbd><b>G</b></kbd> grassetto o <kbd><i>I</i></kbd> italico nell'editor se sei rimasto bloccato.
quoting:
instructions: |-
Puoi provare a citarmi nella tua risposta, così saprò esattamente a quale parte del mio messaggio stai rispondendo?
> Se questo è caffè, per favore portami del thè; ma se questo è thè, per favore portami del caffè.
>
> Un vantaggio del parlare con sè stessi è che almeno sai che qualcuno ti sta ascoltando.
>
> Alcune persone si fanno strada con le parole e altre persone… oh, oh, non hanno una strada.
Seleziona il testo della citazione &uarr; che preferisci, e poi premi il pulsante **Cita** che comparirà sopra il testo selezionato o il pulsante **Rispondi** in fondo a questo messaggio.
Sotto la citazione, digita una o due parole sul motivo per cui hai scelto proprio quella perchè sono curioso :thinking:
reply: |-
Bel lavoro, hai scelto la mia citazione preferita! :left_speech_bubble:
not_found: |-
Hmm sembra che tu non mi abbia citato nella tua risposta!
Selezionare un qualsiasi testo del mio messaggio farà apparire il pulsante <kbd>**Cita**</kbd>. E anche premere **Rispondi** con qualsiasi testo selezionato funzionerà! Puoi provare di nuovo?
bookmark:
instructions: |-
Se vuoi saperne di più, seleziona <img src="/images/font-awesome-ellipsis.png" width="16" height="16"> qui sotto e <img src="/images/font-awesome-bookmark.png" width="16" height="16"> per inserire **questo messaggio privato nei segnalibri**. Se lo farai, ci potrebbe essere un :gift: nel tuo futuro!
reply: |-
Eccellente! Ora potrai tornare facilmente a questa conversazione privata in ogni momento, proprio dalla [scheda segnalibri sul tuo profilo](%{profile_page_url}/activity/bookmarks). Basta selezionare l'immagine del tuo profilo in alto a destra &#8599;
not_found: |-
Oh oh, non vedo nessun segnalibro in questo argomento. Hai trovato il pulsante segnalibro sotto ogni messaggio? Usa mostra altro <img src="/images/font-awesome-ellipsis.png" width="16" height="16"> per rivelare pulsanti aggiuntivi se necessario.
emoji:
instructions: |-
Puoi avermi visto utilizzare delle piccole immagini nelle mie risposte :blue_car::dash: che si chiamano [emoji](https://en.wikipedia.org/wiki/Emoji). Puoi **aggiungere una emoji** alla tua risposta? Uno qualsiasi di questi metodi funzionerà:
- Digita `:) ;) :D :P :O`
- Digita due punti <kbd>:</kbd> poi completa il nome della emoji `:tada:`
- Premi il pulsante emoji <img src="/images/font-awesome-smile.png" width="16" height="16"> nell'editor, o sulla tastiera mobile
reply: |-
Questo è :sparkles: _emojitastico!_ :sparkles:
not_found: |-
Oops, non vedo nessuna emoji nella tua risposta! Oh no! :sob:
Prova digitando i due punti <kbd>:</kbd> per far apparire il selettore delle emoji, poi digita le prime lettere della emoji che vuoi, ad esempio `:bird:`
Oppure premi il pulsante emoji <img src="/images/font-awesome-smile.png" width="16" height="16"> nell'editor.
(Se sei su un dispositivo mobile, puoi anche immettere l'emoji direttamente dalla tastiera.)
mention:
instructions: |-
Qualche volta potresti volere l'attenzione di una persona, anche se non stai rispondendo direttamente a lei. Digita `@` poi completa il suo username per menzionarla.
Puoi menzionare **`@%{discobot_username}`** nella tua risposta?
reply: |-
_Qualcuno ha fatto il mio nome!?_ :raised_hand: Credo che sia stato tu! :wave: Bene, eccomi qui! Grazie per avermi menzionato. :ok_hand:
not_found: |-
Non vedo il mio nome da nessuna parte qui :frowning: Puoi provare a menzionarmi di nuovo come `@%{discobot_username}`?
(E sì, il mio username inizia con _disco_, come nella mania dance degli anni 70. Io [amo la vita notturna!](https://www.youtube.com/watch?v=B_wGI3_sGf8) :dancer:)
flag:
instructions: |-
Ci piacciono le discussioni amichevoli, e abbiamo bisogno del tuo aiuto per [mantenere le cose civilizzate](%{guidelines_url}). Se vedi un problema, per favore segnalalo privatamente per farlo sapere all'autore o [al nostro staff](%{about_url}).
> :imp: Ho scritto qualcosa di brutto qui
Credo che tu sappia cosa fare. Vai avanti e **segnala questo messaggio** <img src="/images/font-awesome-flag.png" width="16" height="16"> come inappropriato!
reply: |-
[Il nostro staff](/groups/staff) verrà notificato privatamente della tua segnalazione. Se abbastanza membri della comunità segnalano un messaggio, quest'ultimo verrà automaticamente nascosto per precauzione. (Dal momento che non ho scritto veramente qualcosa di brutto :angel:, ho rimosso la segnalazione per ora.)
not_found: |-
Oh no, il mio brutto messaggio non è ancora stato segnalato. :worried: Puoi segnalarlo come inappropriato usando il pulsante **segnala** <img src="/images/font-awesome-flag.png" width="16" height="16">? Non dimenticarti di usare il pulsante mostra altro <img src="/images/font-awesome-ellipsis.png" width="16" height="16"> per rivelare altre azioni possibili su ogni messaggio.
search:
instructions: |-
_psst_ … Ho nascosto una sorpresa in una delle mie risposte precedenti. Se sei pronto alla sfida, **seleziona l'icona cerca** <img src="/images/font-awesome-search.png" width="16" height="16"> in alto a destra &#8599; per cercarla.
Prova a cercare il termine "capi&#8203;bara" in questo argomento
hidden_message: |-
Come hai fatto a perdere questo capibara? :wink:
<img src="/images/capybara-eating.gif"/>
Hai notato che sei tornato all'inizio dell'argomento? Dai da mangiare a questo affamato capibara **rispondendo con l'emoji `:herb:`** e verrai automaticamente riportato alla fine.
reply: |-
Hey l'hai trovato :tada:
- Per ricerche più dettagliate, vai alla [pagina di ricerca](%{search_url}).
- Per saltare ovunque in una discussione lunga, prova i controlli temporali sulla destra (e in fondo, su mobile).
- Se hai una :keyboard: fisica, premi <kbd>?</kbd> per visualizzare delle comode scorciatoie da tastiera.
not_found: |-
Hmm… Sembra che tu abbia qualche problema. Ci dispiace. Hai cercato su <img src="/images/font-awesome-search.png" width="16" height="16"> il termine **capi&#8203;bara**?
end:
message: |-
Grazie per avermi seguito @%{username}! Ho fatto questo per te, penso che te lo sei guadagnato:
%{certificate}
E' tutto per ora! Controlla [**gli argomenti delle nostre ultime discussioni**](/latest) or [**le categorie di discussione**](/categories). :sunglasses:
(Se vuoi parlare con me ancora per saperne di più, inviami un messaggio o menzionami `@%{discobot_username}` quando vuoi!)
certificate:
alt: 'Attestato di Merito'
advanced_user_narrative:
reset_trigger: 'utente avanzato'
cert_title: "In riconoscimento del completamento con successo del tutorial utente avanzato"
title: ':arrow_up: Funzioni utente avanzato'
start_message: |-
Come un utente avanzato, hai già visitato [la pagina delle opzioni sul tuo profilo](/my/preferences) @%{username}? Ci sono molti modi per personalizzare la tua esperienza, ad esempio selezionando un tema scuro o uno chiaro.
Ma sto divagando, cominciamo!
edit:
bot_created_post_raw: "@%{discobot_username} è, di gran lunga, il bot più interessante che conosco :wink:"
instructions: |-
Tutti fanno degli errori. Ma non preoccuparti, puoi sempre modificare i tuoi messaggi per sistemarli!
Puoi iniziare **modificando** il messaggio che ho appena creato al tuo posto?
not_found: |-
Sembra che tu non abbia ancora modificato il [messaggio](%{url}) che ho creato per te. Puoi provare di nuovo?
Usa l'icona <img src="/images/font-awesome-pencil.png" width="16" height="16"> per far apparire l'editor.
reply: |-
Ottimo lavoro!
Nota che le modifiche fatte dopo 5 minuti verranno mostrate come revisioni pubbliche, e una piccola icona a forma di matita apparirà in alto a destra sul messaggio con il conteggio delle modifiche fatte.
delete:
instructions: |-
Se lo desideri puoi eliminare un tuo messaggio, cancellandolo.
Vai avanti e **cancella** uno dei tuoi messaggi precedenti usando il pulsante <img src="/images/font-awesome-trash.png" width="16" height="16"> **cancella**. Non cancellare il primo messaggio però!
not_found: |-
Non vedo ancora nessun messaggio cancellato! Ricorda di cliccare su <img src="/images/font-awesome-ellipsis.png" width="16" height="16"> mostra altro per rivelare <img src="/images/font-awesome-trash.png" width="16" height="16"> il pulsante cancella.
reply: |-
Whoa! :boom:
Per preservare la continuità delle discussioni, le cancellazioni non sono immediate, così i messaggi saranno rimossi dopo un certo periodo di tempo.
recover:
deleted_post_raw: 'Perchè @%{discobot_username} ha cancellato il mio messaggio? :anguished:'
instructions: |-
Oh no! Sembra che io abbia accidentalmente cancellato un nuovo messaggio che avevo creato per te.
Puoi farmi un favore e <img src="/images/font-awesome-rotate-left.png" width="16" height="16"> **ripristinarlo**?
not_found: |-
Stai avendo problemi? Ricorda di cliccare su <img src="/images/font-awesome-ellipsis.png" width="16" height="16"> mostra altro per rivelare <img src="/images/font-awesome-rotate-left.png" width="16" height="16"> il pulsante ripristina.
reply: |-
Pfff, questa è fatta! Grazie per averlo ripristinato :wink:
Ti faccio notare che hai solo 24 ore di tempo per ripristinare un messaggio.
category_hashtag:
instructions: |-
Lo sapevi che è possibile fare riferimento a categorie ed etichette in un messaggio? Per esempio, hai visto la categoria %{category}?
Digita `#` nel mezzo di una frase e seleziona una categoria o un'etichetta.
not_found: |-
Hmm, non vedo nessuna categoria. Nota che `#` non può essere il primo carattere di una riga. Puoi copiare questo nella tua prossima risposta?
Posso creare un collegamento ad una categoria digitando #
reply: |-
Eccellente! Ricorda che questo funziona per le categorie _e_ le etichette, se le etichette sono abilitate.
change_topic_notification_level:
instructions: |-
Ogni argomento ha un livello di notifica. Parte da 'normale', il che significa che riceverai una notifica quando qualcuno parlerà direttamente con te.
Per impostazione predefinita, il livello di notifica per un messaggio privato è impostato al livello più alto 'in osservazione', il che significa che riceverai una notifica ad ogni nuovo messaggio. Ma puoi sovrascrivere il livello di notifica per _ogni_ argomento su 'in osservazione', 'seguito' o 'silenziato'.
Prova a modificare il livello di notifica per questo argomento. In fondo alla discussione, troverai un pulsante che ti mostrerà che questo argomento è **in osservazione** . Puoi modificare il livello di notifica a **seguito**?
not_found: |-
Sembra che stia ancora in osservazione :eyes: di questo argomento! Se hai problemi a trovarlo, il pulsante relativo al livello della notifica è situato in fondo a questo argomento.
reply: |-
Lavoro impressionante! Spero che non silenzi questo argomento dato che a volte posso essere un po' loquace :grin:.
Nota che quando rispondi ad un argomento, o leggi un argomento per più di qualche minuto, verrà automaticamente impostato il livello di notifica a 'seguito'. Puoi modificare queste impostazioni sulle [tue preferenze utente](/my/preferences).
poll:
instructions: |-
Lo sapevi che puoi aggiungere un sondaggio in qualsiasi messaggio? Prova a usare l'icona <img src="/images/font-awesome-gear.png" width="16" height="16"> ingranaggio sull'editor per **costruire un sondaggio**.
not_found: |-
Whoops! Non c'è nessun sondaggio nella tua risposta.
Usa l'icona <img src="/images/font-awesome-gear.png" width="16" height="16"> ingranaggio sull'editor, oppure copia e incolla questo sondaggio nella tua prossima risposta:
[poll]
* :cat:
* :dog:
[/poll]
reply: |-
Hey, bel sondaggio! Sono un buon insegnante?
[poll]
* :+1:
* :-1:
[/poll]
details:
instructions: |-
Delle volte desidererai **nascondere dei dettagli** nelle tue risposte:
- Quando stai discutendo alcuni punti della trama di un film o di uno show televisivo che potrebbero essere considerati spoiler.
- Quando il tuo messaggio necessita di molti dettagli opzionali che possono essere di intralcio se letti tutti in una volta.
[details=Seleziona questa opzione per vedere come funziona!]
1. Seleziona l'icona <img src="/images/font-awesome-gear.png" width="16" height="16"> ingranaggio sull'editor.
2. Seleziona "Nascondi Dettagli".
3. Modifica il sommario e aggiungi il tuo contenuto.
[/details]
Puoi usare l'icona <img src="/images/font-awesome-gear.png" width="16" height="16"> ingranaggio sull'editor per aggiungere una sezione con dei dettagli nella tua prossima risposta?
not_found: |-
Hai problemi a creare un widget con dei dettagli? Prova a includere quello che segue nella tua prossima risposta:
[details=Selezionami per vedere i dettagli]
Qui ci sono i dettagli
[/details]
reply: |-
Ottimo lavoro — la tua attenzione per i _dettagli_ è ammirevole!
end:
message: |-
Hai affrontato tutto questo come un _utente avanzato_ infatti :bow:
%{certificate}
Questo è tutto quello che posso fare per te.
Arrivederci per adesso! Se desideri parlare di nuovo con me mandami un messaggio in qualsiasi momento :sunglasses:
certificate:
alt: 'Attestato di Merito per Utente Avanzato'

View File

@ -0,0 +1,23 @@
plugins:
discourse_narrative_bot_enabled:
default:
default: true
test: false
client: true
disable_discourse_narrative_bot_welcome_post:
default: false
discourse_narrative_bot_welcome_post_type:
default: 'new_user_track'
enum: 'DiscourseNarrativeBot::WelcomePostTypeSiteSetting'
discourse_narrative_bot_welcome_post_delay:
default: 0
discourse_narrative_bot_ignored_usernames:
default: 'discourse'
type: list
discourse_narrative_bot_disable_public_replies:
default: false
uncategorized:
send_welcome_message:
default: false
hidden: true

View File

@ -0,0 +1,43 @@
discobot_username ='discobot'
user = User.find_by(id: -2)
if !user
suggested_username = UserNameSuggester.suggest(discobot_username)
User.seed do |u|
u.id = -2
u.name = discobot_username
u.username = suggested_username
u.username_lower = suggested_username.downcase
u.email = "discobot_email"
u.password = SecureRandom.hex
u.active = true
u.approved = true
u.trust_level = TrustLevel[4]
end
# TODO Pull the user avatar from that thread for now. In the future, pull it from a local file or from some central discobot repo.
if !Rails.env.test?
UserAvatar.import_url_for_user(
"https://cdn.discourse.org/dev/uploads/default/original/2X/e/edb63d57a720838a7ce6a68f02ba4618787f2299.png",
User.find(-2),
override_gravatar: true
)
end
end
bot = User.find(-2)
bot.update!(admin:true, moderator: false)
bot.user_option.update!(
email_private_messages: false,
email_direct: false
)
if !bot.user_profile.bio_raw
bot.user_profile.update!(
bio_raw: I18n.t('discourse_narrative_bot.bio', site_title: SiteSetting.title, discobot_username: bot.username)
)
end
Group.user_trust_level_change!(-2, TrustLevel[4])

View File

@ -0,0 +1,39 @@
Badge
.where(name: 'Complete New User Track')
.update_all(name: DiscourseNarrativeBot::NewUserNarrative::BADGE_NAME)
Badge
.where(name: 'Complete Discobot Advanced User Track')
.update_all(name: DiscourseNarrativeBot::AdvancedUserNarrative::BADGE_NAME)
new_user_narrative_badge = Badge.find_by(name: DiscourseNarrativeBot::NewUserNarrative::BADGE_NAME)
unless new_user_narrative_badge
new_user_narrative_badge = Badge.create!(
name: DiscourseNarrativeBot::NewUserNarrative::BADGE_NAME,
badge_type_id: 3
)
end
advanced_user_narrative_badge = Badge.find_by(name: DiscourseNarrativeBot::AdvancedUserNarrative::BADGE_NAME)
unless advanced_user_narrative_badge
advanced_user_narrative_badge = Badge.create!(
name: DiscourseNarrativeBot::AdvancedUserNarrative::BADGE_NAME,
badge_type_id: 2
)
end
badge_grouping = BadgeGrouping.find(1)
[
[new_user_narrative_badge, I18n.t('badges.certified.description')],
[advanced_user_narrative_badge, I18n.t('badges.licensed.description')]
].each do |badge, description|
badge.update!(
badge_grouping: badge_grouping,
description: description,
system: true
)
end

View File

@ -0,0 +1,17 @@
module Jobs
class BotInput < Jobs::Base
sidekiq_options queue: 'critical', retry: false
def execute(args)
return unless user = User.find_by(id: args[:user_id])
I18n.with_locale(user.effective_locale) do
::DiscourseNarrativeBot::TrackSelector.new(args[:input].to_sym, user,
post_id: args[:post_id],
topic_id: args[:topic_id]
).select
end
end
end
end

View File

@ -0,0 +1,13 @@
module Jobs
class NarrativeInit < Jobs::Base
sidekiq_options queue: 'critical'
def execute(args)
if user = User.find_by(id: args[:user_id])
I18n.with_locale(user.effective_locale) do
args[:klass].constantize.new.input(:init, user)
end
end
end
end
end

View File

@ -0,0 +1,11 @@
module Jobs
class NarrativeTimeout < Jobs::Base
def execute(args)
if user = User.find_by(id: args[:user_id])
I18n.with_locale(user.effective_locale) do
args[:klass].constantize.new.notify_timeout(user)
end
end
end
end
end

View File

@ -0,0 +1,35 @@
module Jobs
module DiscourseNarrativeBot
class GrantBadges < ::Jobs::Onceoff
def execute_onceoff(args)
new_user_track_badge = Badge.find_by(
name: ::DiscourseNarrativeBot::NewUserNarrative::BADGE_NAME
)
advanced_user_track_badge = Badge.find_by(
name: ::DiscourseNarrativeBot::AdvancedUserNarrative::BADGE_NAME
)
PluginStoreRow.where(
plugin_name: ::DiscourseNarrativeBot::PLUGIN_NAME,
type_name: 'JSON'
).find_each do |row|
value = JSON.parse(row.value)
completed = value["completed"]
user = User.find_by(id: row.key)
if user && completed
if completed.include?(::DiscourseNarrativeBot::NewUserNarrative.to_s)
BadgeGranter.grant(new_user_track_badge, user)
end
if completed.include?(::DiscourseNarrativeBot::AdvancedUserNarrative.to_s)
BadgeGranter.grant(advanced_user_track_badge, user)
end
end
end
end
end
end
end

View File

@ -0,0 +1,25 @@
module Jobs
class SendDefaultWelcomeMessage < Jobs::Base
def execute(args)
if user = User.find_by(id: args[:user_id])
type = user.invited_by ? 'welcome_invite' : 'welcome_user'
params = SystemMessage.new(user).defaults
title = I18n.t("system_messages.#{type}.subject_template", params)
raw = I18n.t("system_messages.#{type}.text_body_template", params)
discobot_user = User.find(-2)
post = PostCreator.create!(
discobot_user,
title: title,
raw: raw,
archetype: Archetype.private_message,
target_usernames: user.username,
skip_validations: true
)
post.topic.update_status('closed', true, discobot_user)
end
end
end
end

View File

@ -0,0 +1,85 @@
module DiscourseNarrativeBot
module Actions
extend ActiveSupport::Concern
included do
def self.discobot_user
@discobot ||= User.find(-2)
end
end
private
def reply_to(post, raw, opts = {})
if post
default_opts = {
raw: raw,
topic_id: post.topic_id,
reply_to_post_number: post.post_number
}
new_post = PostCreator.create!(self.class.discobot_user, default_opts.merge(opts))
reset_rate_limits(post) if new_post
new_post
else
PostCreator.create!(self.class.discobot_user, { raw: raw }.merge(opts))
end
end
def reset_rate_limits(post)
user = post.user
data = DiscourseNarrativeBot::Store.get(user.id.to_s)
return unless data
key = "#{DiscourseNarrativeBot::PLUGIN_NAME}:reset-rate-limit:#{post.topic_id}:#{data['state']}"
if !(count = $redis.get(key))
count = 0
duration =
if user && user.new_user?
SiteSetting.rate_limit_new_user_create_post
else
SiteSetting.rate_limit_create_post
end
$redis.setex(key, duration, count)
end
if count.to_i < 2
post.default_rate_limiter.rollback!
post.limit_posts_per_day&.rollback!
$redis.incr(key)
end
end
def fake_delay
sleep(rand(2..3)) if Rails.env.production?
end
def bot_mentioned?(post)
doc = Nokogiri::HTML.fragment(post.cooked)
valid = false
doc.css(".mention").each do |mention|
valid = true if mention.text == "@#{self.class.discobot_user.username}"
end
valid
end
def reply_to_bot_post?(post)
post&.reply_to_post && post.reply_to_post.user_id == -2
end
def pm_to_bot?(post)
topic = post.topic
return false if !topic
topic.pm_with_non_human_user? &&
topic.topic_allowed_users.where(user_id: -2).exists?
end
end
end

View File

@ -0,0 +1,384 @@
module DiscourseNarrativeBot
class AdvancedUserNarrative < Base
I18N_KEY = "discourse_narrative_bot.advanced_user_narrative".freeze
BADGE_NAME = 'Licensed'.freeze
TRANSITION_TABLE = {
begin: {
next_state: :tutorial_edit,
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.edit.instructions") },
init: {
action: :start_advanced_track
}
},
tutorial_edit: {
next_state: :tutorial_delete,
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.delete.instructions") },
edit: {
action: :reply_to_edit
},
reply: {
next_state: :tutorial_edit,
action: :missing_edit
}
},
tutorial_delete: {
next_state: :tutorial_recover,
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.recover.instructions") },
delete: {
action: :reply_to_delete
},
reply: {
next_state: :tutorial_delete,
action: :missing_delete
}
},
tutorial_recover: {
next_state: :tutorial_category_hashtag,
next_instructions: Proc.new do
category = Category.secured.last
slug = category.slug
if parent_category = category.parent_category
slug = "#{parent_category.slug}#{CategoryHashtag::SEPARATOR}#{slug}"
end
I18n.t("#{I18N_KEY}.category_hashtag.instructions",
category: "##{slug}"
)
end,
recover: {
action: :reply_to_recover
},
reply: {
next_state: :tutorial_recover,
action: :missing_recover
}
},
tutorial_category_hashtag: {
next_state: :tutorial_change_topic_notification_level,
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.change_topic_notification_level.instructions") },
reply: {
action: :reply_to_category_hashtag
}
},
tutorial_change_topic_notification_level: {
next_state: :tutorial_poll,
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.poll.instructions") },
topic_notification_level_changed: {
action: :reply_to_topic_notification_level_changed
},
reply: {
next_state: :tutorial_notification_level,
action: :missing_topic_notification_level_change
}
},
tutorial_poll: {
next_state: :tutorial_details,
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.details.instructions") },
reply: {
action: :reply_to_poll
}
},
tutorial_details: {
next_state: :end,
reply: {
action: :reply_to_details
}
}
}
def self.can_start?(user)
return true if user.staff?
user.badges.where(name: DiscourseNarrativeBot::NewUserNarrative::BADGE_NAME).exists?
end
def self.reset_trigger
I18n.t('discourse_narrative_bot.advanced_user_narrative.reset_trigger')
end
def reset_bot(user, post)
if pm_to_bot?(post)
reset_data(user, { topic_id: post.topic_id })
else
reset_data(user)
end
Jobs.enqueue_in(2.seconds, :narrative_init, user_id: user.id, klass: self.class.to_s)
end
private
def init_tutorial_edit
data = get_data(@user)
fake_delay
post = PostCreator.create!(@user, {
raw: I18n.t(
"#{I18N_KEY}.edit.bot_created_post_raw",
discobot_username: self.class.discobot_user.username
),
topic_id: data[:topic_id],
skip_bot: true
})
set_state_data(:post_id, post.id)
post
end
def init_tutorial_recover
data = get_data(@user)
post = PostCreator.create!(@user, {
raw: I18n.t(
"#{I18N_KEY}.recover.deleted_post_raw",
discobot_username: self.class.discobot_user.username
),
topic_id: data[:topic_id],
skip_bot: true
})
set_state_data(:post_id, post.id)
PostDestroyer.new(@user, post, skip_bot: true).destroy
end
def start_advanced_track
raw = I18n.t("#{I18N_KEY}.start_message", username: @user.username)
raw = <<~RAW
#{raw}
#{instance_eval(&@next_instructions)}
RAW
opts = {
title: I18n.t("#{I18N_KEY}.title"),
target_usernames: @user.username,
archetype: Archetype.private_message
}
if @post &&
@post.archetype == Archetype.private_message &&
@post.topic.topic_allowed_users.pluck(:user_id).include?(@user.id)
opts = opts.merge(topic_id: @post.topic_id)
end
if @data[:topic_id]
opts = opts.merge(topic_id: @data[:topic_id])
end
post = reply_to(@post, raw, opts)
@data[:topic_id] = post.topic_id
@data[:track] = self.class.to_s
post
end
def reply_to_edit
return unless valid_topic?(@post.topic_id)
fake_delay
raw = <<~RAW
#{I18n.t("#{I18N_KEY}.edit.reply")}
#{instance_eval(&@next_instructions)}
RAW
reply_to(@post, raw)
end
def missing_edit
post_id = get_state_data(:post_id)
return unless valid_topic?(@post.topic_id) && post_id != @post.id
fake_delay
unless @data[:attempted]
reply_to(@post, I18n.t("#{I18N_KEY}.edit.not_found",
url: Post.find_by(id: post_id).url
))
end
enqueue_timeout_job(@user)
false
end
def reply_to_delete
return unless valid_topic?(@topic_id)
fake_delay
raw = <<~RAW
#{I18n.t("#{I18N_KEY}.delete.reply")}
#{instance_eval(&@next_instructions)}
RAW
PostCreator.create!(self.class.discobot_user,
raw: raw,
topic_id: @topic_id
)
end
def missing_delete
return unless valid_topic?(@post.topic_id)
fake_delay
reply_to(@post, I18n.t("#{I18N_KEY}.delete.not_found")) unless @data[:attempted]
enqueue_timeout_job(@user)
false
end
def reply_to_recover
return unless valid_topic?(@post.topic_id)
fake_delay
raw = <<~RAW
#{I18n.t("#{I18N_KEY}.recover.reply")}
#{instance_eval(&@next_instructions)}
RAW
PostCreator.create!(self.class.discobot_user,
raw: raw,
topic_id: @post.topic_id
)
end
def missing_recover
return unless valid_topic?(@post.topic_id) &&
post_id = get_state_data(:post_id) && @post.id != post_id
fake_delay
reply_to(@post, I18n.t("#{I18N_KEY}.recover.not_found")) unless @data[:attempted]
enqueue_timeout_job(@user)
false
end
def reply_to_category_hashtag
topic_id = @post.topic_id
return unless valid_topic?(topic_id)
if Nokogiri::HTML.fragment(@post.cooked).css('.hashtag').size > 0
raw = <<~RAW
#{I18n.t("#{I18N_KEY}.category_hashtag.reply")}
#{instance_eval(&@next_instructions)}
RAW
fake_delay
reply_to(@post, raw)
else
fake_delay
reply_to(@post, I18n.t("#{I18N_KEY}.category_hashtag.not_found")) unless @data[:attempted]
enqueue_timeout_job(@user)
false
end
end
def missing_topic_notification_level_change
return unless valid_topic?(@post.topic_id)
fake_delay
reply_to(@post, I18n.t("#{I18N_KEY}.change_topic_notification_level.not_found")) unless @data[:attempted]
enqueue_timeout_job(@user)
false
end
def reply_to_topic_notification_level_changed
return unless valid_topic?(@topic_id)
fake_delay
raw = <<~RAW
#{I18n.t("#{I18N_KEY}.change_topic_notification_level.reply")}
#{instance_eval(&@next_instructions)}
RAW
fake_delay
post = PostCreator.create!(self.class.discobot_user,
raw: raw,
topic_id: @topic_id
)
enqueue_timeout_job(@user)
post
end
def reply_to_poll
topic_id = @post.topic_id
return unless valid_topic?(topic_id)
if Nokogiri::HTML.fragment(@post.cooked).css(".poll").size > 0
raw = <<~RAW
#{I18n.t("#{I18N_KEY}.poll.reply")}
#{instance_eval(&@next_instructions)}
RAW
fake_delay
reply_to(@post, raw)
else
fake_delay
reply_to(@post, I18n.t("#{I18N_KEY}.poll.not_found")) unless @data[:attempted]
enqueue_timeout_job(@user)
false
end
end
def reply_to_details
topic_id = @post.topic_id
return unless valid_topic?(topic_id)
fake_delay
if Nokogiri::HTML.fragment(@post.cooked).css("details").size > 0
reply_to(@post, I18n.t("#{I18N_KEY}.details.reply"))
else
reply_to(@post, I18n.t("#{I18N_KEY}.details.not_found")) unless @data[:attempted]
enqueue_timeout_job(@user)
false
end
end
def reply_to_wiki
topic_id = @post.topic_id
return unless valid_topic?(topic_id)
fake_delay
if @post.wiki
reply_to(@post, I18n.t("#{I18N_KEY}.wiki.reply"))
else
reply_to(@post, I18n.t("#{I18N_KEY}.wiki.not_found")) unless @data[:attempted]
enqueue_timeout_job(@user)
false
end
end
def end_reply
fake_delay
reply_to(@post, I18n.t("#{I18N_KEY}.end.message",
certificate: certificate('advanced')
))
end
def synchronize(user)
if Rails.env.test?
yield
else
DistributedMutex.synchronize("advanced_user_narrative_#{user.id}") { yield }
end
end
end
end

View File

@ -0,0 +1,192 @@
module DiscourseNarrativeBot
class Base
include Actions
TIMEOUT_DURATION = 900 # 15 mins
class InvalidTransitionError < StandardError; end
def input(input, user, post: nil, topic_id: nil, skip: false)
new_post = nil
@post = post
@topic_id = topic_id
@skip = skip
synchronize(user) do
@user = user
@data = get_data(user) || {}
@state = (@data[:state] && @data[:state].to_sym) || :begin
@input = input
opts = {}
begin
opts = transition
rescue InvalidTransitionError
# For given input, no transition for current state
return
end
next_state = opts[:next_state]
action = opts[:action]
if next_instructions = opts[:next_instructions]
@next_instructions = next_instructions
end
begin
old_data = @data.dup
new_post = (@skip && @state != :end) ? skip_tutorial(next_state) : self.send(action)
if new_post
old_state = old_data[:state]
state_changed = (old_state.to_s != next_state.to_s)
clean_up_state(old_state) if state_changed
@state = @data[:state] = next_state
@data[:last_post_id] = new_post.id
set_data(@user, @data)
init_state(next_state) if state_changed
if next_state == :end
end_reply
cancel_timeout_job(user)
BadgeGranter.grant(
Badge.find_by(name: self.class::BADGE_NAME),
user
)
set_data(@user,
topic_id: new_post.topic_id,
state: :end,
track: self.class.to_s
)
end
end
rescue => e
@data = old_data
set_data(@user, @data)
raise e
end
end
new_post
end
def reset_bot
not_implemented
end
def set_data(user, value)
DiscourseNarrativeBot::Store.set(user.id, value)
end
def get_data(user)
DiscourseNarrativeBot::Store.get(user.id)
end
def notify_timeout(user)
@data = get_data(user) || {}
if post = Post.find_by(id: @data[:last_post_id])
reply_to(post, I18n.t("discourse_narrative_bot.timeout.message",
username: user.username,
skip_trigger: TrackSelector.skip_trigger,
reset_trigger: "#{TrackSelector.reset_trigger} #{self.class.reset_trigger}",
))
end
end
def certificate(type = nil)
options = {
user_id: @user.id,
date: Time.zone.now.strftime('%b %d %Y'),
host: Discourse.base_url,
format: :svg
}
options.merge!(type: type) if type
src = DiscourseNarrativeBot::Engine.routes.url_helpers.certificate_url(options)
"<img class='discobot-certificate' src='#{src}' width='650' height='464' alt='#{I18n.t("#{self.class::I18N_KEY}.certificate.alt")}'>"
end
protected
def set_state_data(key, value)
@data[@state] ||= {}
@data[@state][key] = value
set_data(@user, @data)
end
def get_state_data(key)
@data[@state] ||= {}
@data[@state][key]
end
def reset_data(user, additional_data = {})
old_data = get_data(user)
new_data = additional_data
set_data(user, new_data)
new_data
end
def transition
options = self.class::TRANSITION_TABLE.fetch(@state).dup
input_options = options.fetch(@input)
options.merge!(input_options) unless @skip
options
rescue KeyError
raise InvalidTransitionError.new
end
def skip_tutorial(next_state)
return unless valid_topic?(@post.topic_id)
fake_delay
if next_state != :end
reply = reply_to(@post, instance_eval(&@next_instructions))
enqueue_timeout_job(@user)
reply
else
@post
end
end
def valid_topic?(topic_id)
topic_id == @data[:topic_id]
end
def cancel_timeout_job(user)
Jobs.cancel_scheduled_job(:narrative_timeout, user_id: user.id, klass: self.class.to_s)
end
def enqueue_timeout_job(user)
return if Rails.env.test?
cancel_timeout_job(user)
Jobs.enqueue_in(TIMEOUT_DURATION, :narrative_timeout,
user_id: user.id,
klass: self.class.to_s
)
end
def not_implemented
raise 'Not implemented.'
end
private
def clean_up_state(state)
clean_up_method = "clean_up_#{state}"
self.send(clean_up_method) if self.class.private_method_defined?(clean_up_method)
end
def init_state(state)
init_method = "init_#{state}"
self.send(init_method) if self.class.private_method_defined?(init_method)
end
end
end

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,32 @@
module DiscourseNarrativeBot
class Dice
MAXIMUM_NUM_OF_DICE = 20
MAXIMUM_RANGE_OF_DICE = 120
def self.roll(num_of_dice, range_of_dice)
if num_of_dice == 0 || range_of_dice == 0
return I18n.t('discourse_narrative_bot.dice.invalid')
end
output = ''
if num_of_dice > MAXIMUM_NUM_OF_DICE
output << I18n.t('discourse_narrative_bot.dice.not_enough_dice',
num_of_dice: MAXIMUM_NUM_OF_DICE
)
output << "\n\n"
num_of_dice = MAXIMUM_NUM_OF_DICE
end
if range_of_dice > MAXIMUM_RANGE_OF_DICE
output << I18n.t('discourse_narrative_bot.dice.out_of_range')
output << "\n\n"
range_of_dice = MAXIMUM_RANGE_OF_DICE
end
output << I18n.t('discourse_narrative_bot.dice.results',
results: num_of_dice.times.map { rand(1..range_of_dice) }.join(", ")
)
end
end
end

View File

@ -0,0 +1,9 @@
module DiscourseNarrativeBot
class Magic8Ball
def self.generate_answer
I18n.t("discourse_narrative_bot.magic_8_ball.result", result: I18n.t(
"discourse_narrative_bot.magic_8_ball.answers.#{rand(1..20)}"
))
end
end
end

View File

@ -0,0 +1,519 @@
require 'distributed_mutex'
module DiscourseNarrativeBot
class NewUserNarrative < Base
I18N_KEY = "discourse_narrative_bot.new_user_narrative".freeze
BADGE_NAME = 'Certified'.freeze
TRANSITION_TABLE = {
begin: {
init: {
next_state: :tutorial_bookmark,
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.bookmark.instructions") },
action: :say_hello
}
},
tutorial_bookmark: {
next_state: :tutorial_onebox,
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.onebox.instructions") },
bookmark: {
action: :reply_to_bookmark
},
reply: {
next_state: :tutorial_bookmark,
action: :missing_bookmark
}
},
tutorial_onebox: {
next_state: :tutorial_emoji,
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.emoji.instructions") },
reply: {
action: :reply_to_onebox
}
},
tutorial_emoji: {
next_state: :tutorial_mention,
next_instructions: Proc.new {
I18n.t("#{I18N_KEY}.mention.instructions", discobot_username: self.class.discobot_user.username)
},
reply: {
action: :reply_to_emoji
}
},
tutorial_mention: {
next_state: :tutorial_formatting,
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.formatting.instructions") },
reply: {
action: :reply_to_mention
}
},
tutorial_formatting: {
next_state: :tutorial_quote,
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.quoting.instructions") },
reply: {
action: :reply_to_formatting
}
},
tutorial_quote: {
next_state: :tutorial_images,
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.images.instructions") },
reply: {
action: :reply_to_quote
}
},
tutorial_images: {
next_state: :tutorial_flag,
next_instructions: Proc.new {
I18n.t("#{I18N_KEY}.flag.instructions",
guidelines_url: url_helpers(:guidelines_url),
about_url: url_helpers(:about_index_url))
},
reply: {
action: :reply_to_image
},
like: {
action: :track_like
}
},
tutorial_flag: {
next_state: :tutorial_search,
next_instructions: Proc.new { I18n.t("#{I18N_KEY}.search.instructions") },
flag: {
action: :reply_to_flag
},
reply: {
next_state: :tutorial_flag,
action: :missing_flag
}
},
tutorial_search: {
next_state: :end,
reply: {
action: :reply_to_search
}
}
}
SEARCH_ANSWER = ':herb:'.freeze
def self.reset_trigger
I18n.t('discourse_narrative_bot.new_user_narrative.reset_trigger')
end
def reset_bot(user, post)
if pm_to_bot?(post)
reset_data(user, { topic_id: post.topic_id })
else
reset_data(user)
end
Jobs.enqueue_in(2.seconds, :narrative_init, user_id: user.id, klass: self.class.to_s)
end
private
def synchronize(user)
if Rails.env.test?
yield
else
DistributedMutex.synchronize("new_user_narrative_#{user.id}") { yield }
end
end
def init_tutorial_search
topic = @post.topic
post = topic.first_post
MessageBus.publish('/new_user_narrative/tutorial_search', {}, user_ids: [@user.id])
raw = <<~RAW
#{post.raw}
#{I18n.t("#{I18N_KEY}.search.hidden_message")}
RAW
PostRevisor.new(post, topic).revise!(
self.class.discobot_user,
{ raw: raw },
{ skip_validations: true, force_new_version: true }
)
set_state_data(:post_version, post.reload.version || 0)
end
def clean_up_tutorial_search
first_post = @post.topic.first_post
first_post.revert_to(get_state_data(:post_version) - 1)
first_post.save!
first_post.publish_change_to_clients!(:revised)
end
def say_hello
raw = I18n.t(
"#{I18N_KEY}.hello.message",
username: @user.username,
title: SiteSetting.title
)
raw = <<~RAW
#{raw}
#{instance_eval(&@next_instructions)}
RAW
opts = {
title: I18n.t("#{I18N_KEY}.hello.title", title: SiteSetting.title),
target_usernames: @user.username,
archetype: Archetype.private_message
}
if @post &&
@post.archetype == Archetype.private_message &&
@post.topic.topic_allowed_users.pluck(:user_id).include?(@user.id)
opts = opts.merge(topic_id: @post.topic_id)
end
if @data[:topic_id]
opts = opts.merge(topic_id: @data[:topic_id])
end
post = reply_to(@post, raw, opts)
@data[:topic_id] = post.topic.id
@data[:track] = self.class.to_s
post
end
def missing_bookmark
return unless valid_topic?(@post.topic_id)
return if @post.user_id == self.class.discobot_user.id
fake_delay
enqueue_timeout_job(@user)
reply_to(@post, I18n.t("#{I18N_KEY}.bookmark.not_found")) unless @data[:attempted]
false
end
def reply_to_bookmark
return unless valid_topic?(@post.topic_id)
return unless @post.user_id == self.class.discobot_user.id
raw = <<~RAW
#{I18n.t("#{I18N_KEY}.bookmark.reply", profile_page_url: url_helpers(:user_url, username: @user.username))}
#{instance_eval(&@next_instructions)}
RAW
fake_delay
reply = reply_to(@post, raw)
enqueue_timeout_job(@user)
reply
end
def reply_to_onebox
post_topic_id = @post.topic_id
return unless valid_topic?(post_topic_id)
@post.post_analyzer.cook(@post.raw, {})
if @post.post_analyzer.found_oneboxes?
raw = <<~RAW
#{I18n.t("#{I18N_KEY}.onebox.reply")}
#{instance_eval(&@next_instructions)}
RAW
fake_delay
reply = reply_to(@post, raw)
enqueue_timeout_job(@user)
reply
else
fake_delay
reply_to(@post, I18n.t("#{I18N_KEY}.onebox.not_found")) unless @data[:attempted]
enqueue_timeout_job(@user)
false
end
end
def track_like
post_topic_id = @post.topic_id
return unless valid_topic?(post_topic_id)
post_liked = PostAction.find_by(
post_action_type_id: PostActionType.types[:like],
post_id: @data[:last_post_id],
user_id: @user.id
)
if post_liked
set_state_data(:liked, true)
if (post_id = get_state_data(:post_id)) && (post = Post.find_by(id: post_id))
fake_delay
like_post(post)
raw = <<~RAW
#{I18n.t("#{I18N_KEY}.images.reply")}
#{instance_eval(&@next_instructions)}
RAW
reply = reply_to(@post, raw)
enqueue_timeout_job(@user)
return reply
end
end
false
end
def reply_to_image
post_topic_id = @post.topic_id
return unless valid_topic?(post_topic_id)
@post.post_analyzer.cook(@post.raw, {})
transition = true
attempted_count = get_state_data(:attempted) || 0
if attempted_count < 2
@data[:skip_attempted] = true
@data[:attempted] = false
else
@data[:skip_attempted] = false
end
if @post.post_analyzer.image_count > 0
set_state_data(:post_id, @post.id)
if get_state_data(:liked)
raw = <<~RAW
#{I18n.t("#{I18N_KEY}.images.reply")}
#{instance_eval(&@next_instructions)}
RAW
like_post(@post)
else
raw = I18n.t(
"#{I18N_KEY}.images.like_not_found",
url: Post.find_by(id: @data[:last_post_id]).url
)
transition = false
end
else
raw = I18n.t(
"#{I18N_KEY}.images.not_found",
image_url: "#{Discourse.base_url}/images/dog-walk.gif"
)
transition = false
end
fake_delay
set_state_data(:attempted, attempted_count + 1) if !transition
reply = reply_to(@post, raw) unless @data[:attempted] && !transition
enqueue_timeout_job(@user)
transition ? reply : false
end
def reply_to_formatting
post_topic_id = @post.topic_id
return unless valid_topic?(post_topic_id)
if Nokogiri::HTML.fragment(@post.cooked).css("b", "strong", "em", "i", ".bbcode-i", ".bbcode-b").size > 0
raw = <<~RAW
#{I18n.t("#{I18N_KEY}.formatting.reply")}
#{instance_eval(&@next_instructions)}
RAW
fake_delay
reply = reply_to(@post, raw)
enqueue_timeout_job(@user)
reply
else
fake_delay
reply_to(@post, I18n.t("#{I18N_KEY}.formatting.not_found")) unless @data[:attempted]
enqueue_timeout_job(@user)
false
end
end
def reply_to_quote
post_topic_id = @post.topic_id
return unless valid_topic?(post_topic_id)
doc = Nokogiri::HTML.fragment(@post.cooked)
if doc.css(".quote").size > 0
raw = <<~RAW
#{I18n.t("#{I18N_KEY}.quoting.reply")}
#{instance_eval(&@next_instructions)}
RAW
fake_delay
reply = reply_to(@post, raw)
enqueue_timeout_job(@user)
reply
else
fake_delay
reply_to(@post, I18n.t("#{I18N_KEY}.quoting.not_found")) unless @data[:attempted]
enqueue_timeout_job(@user)
false
end
end
def reply_to_emoji
post_topic_id = @post.topic_id
return unless valid_topic?(post_topic_id)
doc = Nokogiri::HTML.fragment(@post.cooked)
if doc.css(".emoji").size > 0
raw = <<~RAW
#{I18n.t("#{I18N_KEY}.emoji.reply")}
#{instance_eval(&@next_instructions)}
RAW
fake_delay
reply = reply_to(@post, raw)
enqueue_timeout_job(@user)
reply
else
fake_delay
reply_to(@post, I18n.t("#{I18N_KEY}.emoji.not_found")) unless @data[:attempted]
enqueue_timeout_job(@user)
false
end
end
def reply_to_mention
post_topic_id = @post.topic_id
return unless valid_topic?(post_topic_id)
if bot_mentioned?(@post)
raw = <<~RAW
#{I18n.t("#{I18N_KEY}.mention.reply")}
#{instance_eval(&@next_instructions)}
RAW
fake_delay
reply = reply_to(@post, raw)
enqueue_timeout_job(@user)
reply
else
fake_delay
unless @data[:attempted]
reply_to(@post, I18n.t(
"#{I18N_KEY}.mention.not_found",
username: @user.username,
discobot_username: self.class.discobot_user.username
))
end
enqueue_timeout_job(@user)
false
end
end
def missing_flag
return unless valid_topic?(@post.topic_id)
return if @post.user_id == -2
fake_delay
reply_to(@post, I18n.t("#{I18N_KEY}.flag.not_found")) unless @data[:attempted]
false
end
def reply_to_flag
post_topic_id = @post.topic_id
return unless valid_topic?(post_topic_id)
return unless @post.user.id == -2
raw = <<~RAW
#{I18n.t("#{I18N_KEY}.flag.reply")}
#{instance_eval(&@next_instructions)}
RAW
fake_delay
reply = reply_to(@post, raw)
@post.post_actions.where(user_id: @user.id).destroy_all
enqueue_timeout_job(@user)
reply
end
def reply_to_search
post_topic_id = @post.topic_id
return unless valid_topic?(post_topic_id)
if @post.raw.match(/#{SEARCH_ANSWER}/)
fake_delay
reply_to(@post, I18n.t("#{I18N_KEY}.search.reply", search_url: url_helpers(:search_url)))
else
fake_delay
reply_to(@post, I18n.t("#{I18N_KEY}.search.not_found")) unless @data[:attempted]
enqueue_timeout_job(@user)
false
end
end
def end_reply
fake_delay
reply_to(
@post,
I18n.t("#{I18N_KEY}.end.message",
username: @user.username,
base_url: Discourse.base_url,
certificate: certificate,
discobot_username: self.class.discobot_user.username,
advanced_trigger: AdvancedUserNarrative.reset_trigger
),
topic_id: @data[:topic_id]
)
end
def like_post(post)
PostAction.act(self.class.discobot_user, post, PostActionType.types[:like])
end
def welcome_topic
Topic.find_by(slug: 'welcome-to-discourse', archetype: Archetype.default) ||
Topic.recent(1).first
end
def url_helpers(url, opts = {})
Rails.application.routes.url_helpers.send(url, opts.merge(host: Discourse.base_url))
end
end
end

View File

@ -0,0 +1,27 @@
require 'excon'
module DiscourseNarrativeBot
class QuoteGenerator
API_ENDPOINT = 'http://api.forismatic.com/api/1.0/'.freeze
def self.generate(user)
quote, author =
if user.effective_locale != 'en'
translation_key = "discourse_narrative_bot.quote.#{rand(1..10)}"
[
I18n.t("#{translation_key}.quote"),
I18n.t("#{translation_key}.author")
]
else
connection = Excon.new("#{API_ENDPOINT}?lang=en&format=json&method=getQuote")
response = connection.request(expects: [200, 201], method: :Get)
response_body = JSON.parse(response.body)
[response_body["quoteText"].strip, response_body["quoteAuthor"].strip]
end
I18n.t('discourse_narrative_bot.quote.results', quote: quote, author: author)
end
end
end

View File

@ -0,0 +1,266 @@
module DiscourseNarrativeBot
class TrackSelector
include Actions
GENERIC_REPLIES_COUNT_PREFIX = 'discourse-narrative-bot:track-selector-count:'.freeze
PUBLIC_DISPLAY_BOT_HELP_KEY = 'discourse-narrative-bot:track-selector:display-bot-help'.freeze
TRACKS = [
NewUserNarrative,
AdvancedUserNarrative
]
TOPIC_ACTIONS = [
:delete,
:topic_notification_level_changed
].each(&:freeze)
RESET_TRIGGER_EXACT_MATCH_LENGTH = 200
def initialize(input, user, post_id:, topic_id: nil)
@input = input
@user = user
@post_id = post_id
@topic_id = topic_id
@post = Post.find_by(id: post_id)
end
def select
data = Store.get(@user.id)
if @post && !is_topic_action?
is_reply = @input == :reply
return if is_reply && reset_track
topic_id = @post.topic_id
if (data && data[:topic_id] == topic_id)
state = data[:state]
klass = (data[:track] || NewUserNarrative.to_s).constantize
if is_reply && like_user_post
Store.set(@user.id, data.merge!(state: nil, topic_id: nil))
elsif state&.to_sym == :end && is_reply
bot_commands(bot_mentioned?) || generic_replies(klass.reset_trigger)
elsif is_reply
previous_status = data[:attempted]
current_status = klass.new.input(@input, @user, post: @post, skip: skip_track?)
data = Store.get(@user.id)
data[:attempted] = !current_status
if previous_status && data[:attempted] == previous_status && !data[:skip_attempted]
generic_replies(klass.reset_trigger, state)
else
$redis.del(generic_replies_key(@user))
end
Store.set(@user.id, data)
else
klass.new.input(@input, @user, post: @post, skip: skip_track?)
end
elsif is_reply && (pm_to_bot?(@post) || public_reply?)
like_user_post
bot_commands
end
elsif data && data.dig(:state)&.to_sym != :end && is_topic_action?
klass = (data[:track] || NewUserNarrative.to_s).constantize
klass.new.input(@input, @user, post: @post, topic_id: @topic_id)
end
end
def self.reset_trigger
I18n.t(i18n_key("reset_trigger"))
end
def self.skip_trigger
I18n.t(i18n_key("skip_trigger"))
end
def self.help_trigger
I18n.t(i18n_key("help_trigger"))
end
def self.quote_trigger
I18n.t("discourse_narrative_bot.quote.trigger")
end
def self.dice_trigger
I18n.t("discourse_narrative_bot.dice.trigger")
end
def self.magic_8_ball_trigger
I18n.t("discourse_narrative_bot.magic_8_ball.trigger")
end
private
def is_topic_action?
@is_topic_action ||= TOPIC_ACTIONS.include?(@input)
end
def reset_track
reset = false
TRACKS.each do |klass|
if selected_track(klass)
klass.new.reset_bot(@user, @post)
reset = true
break
end
end
reset
end
def selected_track(klass)
return if klass.respond_to?(:can_start?) && !klass.can_start?(@user)
post_raw = @post.raw
trigger = "#{self.class.reset_trigger} #{klass.reset_trigger}"
if post_raw.length < RESET_TRIGGER_EXACT_MATCH_LENGTH && pm_to_bot?(@post)
post_raw.match(Regexp.new("\\b\\W\?#{trigger}\\W\?\\b", 'i'))
else
match_trigger?(trigger)
end
end
def bot_commands(hint = true)
raw =
if match_data = match_trigger?("#{self.class.dice_trigger} (\\d+)d(\\d+)")
DiscourseNarrativeBot::Dice.roll(match_data[1].to_i, match_data[2].to_i)
elsif match_trigger?(self.class.quote_trigger)
DiscourseNarrativeBot::QuoteGenerator.generate(@user)
elsif match_trigger?(self.class.magic_8_ball_trigger)
DiscourseNarrativeBot::Magic8Ball.generate_answer
elsif match_trigger?(self.class.help_trigger)
help_message
elsif hint
message = I18n.t(self.class.i18n_key('random_mention.reply'),
discobot_username: self.class.discobot_user.username,
help_trigger: self.class.help_trigger
)
if public_reply?
key = "#{PUBLIC_DISPLAY_BOT_HELP_KEY}:#{@post.topic_id}"
last_bot_help_post_number = $redis.get(key)
if !last_bot_help_post_number ||
(last_bot_help_post_number &&
@post.post_number - 10 > last_bot_help_post_number.to_i &&
(1.day.to_i - $redis.ttl(key)) > 6.hours.to_i)
$redis.setex(key, 1.day.to_i, @post.post_number)
message
end
else
message
end
end
if raw
fake_delay
reply_to(@post, raw, skip_validations: true)
end
end
def help_message
tracks = [NewUserNarrative.reset_trigger]
if @user.staff? ||
@user.badges.where(name: DiscourseNarrativeBot::NewUserNarrative::BADGE_NAME).exists?
tracks << AdvancedUserNarrative.reset_trigger
end
discobot_username = self.class.discobot_user.username
message = I18n.t(
self.class.i18n_key('random_mention.tracks'),
discobot_username: discobot_username,
reset_trigger: self.class.reset_trigger,
default_track: NewUserNarrative.reset_trigger,
tracks: tracks.join(', ')
)
message << "\n\n#{I18n.t(self.class.i18n_key('random_mention.bot_actions'),
discobot_username: discobot_username,
dice_trigger: self.class.dice_trigger,
quote_trigger: self.class.quote_trigger,
magic_8_ball_trigger: self.class.magic_8_ball_trigger
)}"
end
def generic_replies_key(user)
"#{GENERIC_REPLIES_COUNT_PREFIX}#{user.id}"
end
def generic_replies(track_reset_trigger, state = nil)
reset_trigger = "#{self.class.reset_trigger} #{track_reset_trigger}"
key = generic_replies_key(@user)
count = ($redis.get(key) || $redis.setex(key, 900, 0)).to_i
case count
when 0
raw = I18n.t(self.class.i18n_key('do_not_understand.first_response'))
if state && state.to_sym != :end
raw = "#{raw}\n\n#{I18n.t(self.class.i18n_key('do_not_understand.track_response'), reset_trigger: reset_trigger, skip_trigger: self.class.skip_trigger)}"
end
reply_to(@post, raw)
when 1
reply_to(@post, I18n.t(self.class.i18n_key('do_not_understand.second_response'),
reset_trigger: self.class.reset_trigger
))
else
# Stay out of the user's way
end
$redis.incr(key)
end
def self.i18n_key(key)
"discourse_narrative_bot.track_selector.#{key}"
end
def skip_track?
if pm_to_bot?(@post)
post_raw = @post.raw
post_raw.match(/^@#{self.class.discobot_user.username} #{self.class.skip_trigger}/i) ||
post_raw.strip == self.class.skip_trigger
else
false
end
end
def match_trigger?(trigger)
discobot_username = self.class.discobot_user.username
regexp = Regexp.new("<a class=\"mention\".*>@#{discobot_username}</a> #{trigger}", 'i')
match = @post.cooked.match(regexp)
if pm_to_bot?(@post)
match || @post.raw.strip.match(Regexp.new("^#{trigger}$", 'i'))
else
match
end
end
def like_user_post
if @post.raw.match(/thank/i)
PostAction.act(self.class.discobot_user, @post, PostActionType.types[:like])
end
end
def bot_mentioned?
@bot_mentioned ||= PostAnalyzer.new(@post.raw, @post.topic_id).raw_mentions.include?(
self.class.discobot_user.username
)
end
def public_reply?
!SiteSetting.discourse_narrative_bot_disable_public_replies &&
(bot_mentioned? || reply_to_bot_post?(@post))
end
end
end

View File

@ -0,0 +1,18 @@
module DiscourseNarrativeBot
class WelcomePostTypeSiteSetting
def self.valid_value?(val)
values.any? { |v| v[:value] == val.to_s }
end
def self.values
@values ||= [
{ name: 'discourse_narrative_bot.welcome_post_type.new_user_track', value: 'new_user_track' },
{ name: 'discourse_narrative_bot.welcome_post_type.welcome_message', value: 'welcome_message' }
]
end
def self.translate_names?
true
end
end
end

View File

@ -0,0 +1,223 @@
# name: discourse-narrative-bot
# about: Introduces staff to Discourse
# version: 0.0.1
# authors: Nick Sahler (@nicksahler)
enabled_site_setting :discourse_narrative_bot_enabled
if Rails.env.development?
Rails.application.config.before_initialize do |app|
app.middleware.insert_before(
::ActionDispatch::Static,
::ActionDispatch::Static,
Rails.root.join("plugins/discourse-narrative-bot/public").to_s
)
end
end
require_relative 'lib/discourse_narrative_bot/welcome_post_type_site_setting.rb'
after_initialize do
SeedFu.fixture_paths << Rails.root.join("plugins", "discourse-narrative-bot", "db", "fixtures").to_s
Mime::Type.register "image/svg+xml", :svg
[
'../jobs/bot_input.rb',
'../jobs/narrative_timeout.rb',
'../jobs/narrative_init.rb',
'../jobs/send_default_welcome_message.rb',
'../jobs/onceoff/grant_badges.rb',
'../lib/discourse_narrative_bot/actions.rb',
'../lib/discourse_narrative_bot/base.rb',
'../lib/discourse_narrative_bot/new_user_narrative.rb',
'../lib/discourse_narrative_bot/advanced_user_narrative.rb',
'../lib/discourse_narrative_bot/track_selector.rb',
'../lib/discourse_narrative_bot/certificate_generator.rb',
'../lib/discourse_narrative_bot/dice.rb',
'../lib/discourse_narrative_bot/quote_generator.rb',
'../lib/discourse_narrative_bot/magic_8_ball.rb',
'../lib/discourse_narrative_bot/welcome_post_type_site_setting.rb'
].each { |path| load File.expand_path(path, __FILE__) }
# Disable welcome message because that is what the bot is supposed to replace.
SiteSetting.send_welcome_message = false
module ::DiscourseNarrativeBot
PLUGIN_NAME = "discourse-narrative-bot".freeze
class Engine < ::Rails::Engine
engine_name PLUGIN_NAME
isolate_namespace DiscourseNarrativeBot
if Rails.env.production?
Dir[Rails.root.join("plugins/discourse-narrative-bot/public/images/*")].each do |src|
dest = Rails.root.join("public/images/#{File.basename(src)}")
File.symlink(src, dest) if !File.exists?(dest)
end
end
end
class Store
def self.set(key, value)
::PluginStore.set(PLUGIN_NAME, key, value)
end
def self.get(key)
::PluginStore.get(PLUGIN_NAME, key)
end
def self.remove(key)
::PluginStore.remove(PLUGIN_NAME, key)
end
end
class CertificatesController < ::ApplicationController
layout :false
skip_before_filter :check_xhr
def generate
raise Discourse::InvalidParameters.new('user_id must be present') unless params[:user_id]&.present?
user = User.find_by(id: params[:user_id])
raise Discourse::NotFound if user.blank?
raise Discourse::InvalidParameters.new('date must be present') unless params[:date]&.present?
generator = CertificateGenerator.new(user, params[:date])
svg =
case params[:type]
when 'advanced'
generator.advanced_user_track
else
generator.new_user_track
end
respond_to do |format|
format.svg { render inline: svg}
end
end
end
end
DiscourseNarrativeBot::Engine.routes.draw do
get "/certificate" => "certificates#generate", format: :svg
end
Discourse::Application.routes.append do
mount ::DiscourseNarrativeBot::Engine, at: "/discobot"
end
self.add_model_callback(User, :after_destroy) do
DiscourseNarrativeBot::Store.remove(self.id)
end
self.add_model_callback(User, :after_commit, on: :create) do
return if SiteSetting.disable_discourse_narrative_bot_welcome_post
delay = SiteSetting.discourse_narrative_bot_welcome_post_delay
case SiteSetting.discourse_narrative_bot_welcome_post_type
when 'new_user_track'
if enqueue_narrative_bot_job?
Jobs.enqueue_in(delay, :narrative_init,
user_id: self.id,
klass: DiscourseNarrativeBot::NewUserNarrative.to_s
)
end
when 'welcome_message'
Jobs.enqueue_in(delay, :send_default_welcome_message, user_id: self.id)
end
end
require_dependency "user"
User.class_eval do
def enqueue_narrative_bot_job?
SiteSetting.discourse_narrative_bot_enabled &&
self.id > 0 &&
!self.anonymous? &&
!self.user_option.mailing_list_mode &&
!self.staged &&
!SiteSetting.discourse_narrative_bot_ignored_usernames.split('|'.freeze).include?(self.username)
end
end
self.on(:post_created) do |post, options|
user = post.user
if user.enqueue_narrative_bot_job? && !options[:skip_bot]
Jobs.enqueue(:bot_input,
user_id: user.id,
post_id: post.id,
input: :reply
)
end
end
self.on(:post_edited) do |post|
if post.user.enqueue_narrative_bot_job?
Jobs.enqueue(:bot_input,
user_id: post.user.id,
post_id: post.id,
input: :edit
)
end
end
self.on(:post_destroyed) do |post, options, user|
if user.enqueue_narrative_bot_job? && !options[:skip_bot]
Jobs.enqueue(:bot_input,
user_id: user.id,
post_id: post.id,
topic_id: post.topic_id,
input: :delete
)
end
end
self.on(:post_recovered) do |post, _, user|
if user.enqueue_narrative_bot_job?
Jobs.enqueue(:bot_input,
user_id: user.id,
post_id: post.id,
input: :recover
)
end
end
self.add_model_callback(PostAction, :after_commit, on: :create) do
if self.user.enqueue_narrative_bot_job?
input =
case self.post_action_type_id
when *PostActionType.flag_types.values
:flag
when PostActionType.types[:like]
:like
when PostActionType.types[:bookmark]
:bookmark
end
if input
Jobs.enqueue(:bot_input,
user_id: self.user.id,
post_id: self.post.id,
input: input
)
end
end
end
self.on(:topic_notification_level_changed) do |_, user_id, topic_id|
user = User.find_by(id: user_id)
if user && user.enqueue_narrative_bot_job?
Jobs.enqueue(:bot_input,
user_id: user_id,
topic_id: topic_id,
input: :topic_notification_level_changed
)
end
end
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 989 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 940 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 972 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

View File

@ -0,0 +1,658 @@
require 'rails_helper'
RSpec.describe DiscourseNarrativeBot::AdvancedUserNarrative do
let(:discobot_user) { User.find(-2) }
let(:first_post) { Fabricate(:post, user: discobot_user) }
let(:user) { Fabricate(:user) }
let(:topic) do
Fabricate(:private_message_topic, first_post: first_post,
topic_allowed_users: [
Fabricate.build(:topic_allowed_user, user: discobot_user),
Fabricate.build(:topic_allowed_user, user: user),
]
)
end
let(:post) { Fabricate(:post, topic: topic, user: user) }
let(:narrative) { described_class.new }
let(:other_topic) { Fabricate(:topic) }
let(:other_post) { Fabricate(:post, topic: other_topic) }
let(:skip_trigger) { DiscourseNarrativeBot::TrackSelector.skip_trigger }
let(:reset_trigger) { DiscourseNarrativeBot::TrackSelector.reset_trigger }
before do
SiteSetting.discourse_narrative_bot_enabled = true
end
describe '.can_start?' do
describe 'when user is a moderator' do
it 'should return true' do
user.update!(moderator: true)
expect(described_class.can_start?(user)).to eq(true)
end
end
end
describe '#notify_timeout' do
before do
narrative.set_data(user,
state: :tutorial_poll,
topic_id: topic.id,
last_post_id: post.id
)
end
it 'should create the right message' do
expect { narrative.notify_timeout(user) }.to change { Post.count }.by(1)
expect(Post.last.raw).to eq(I18n.t(
'discourse_narrative_bot.timeout.message',
username: user.username,
skip_trigger: skip_trigger,
reset_trigger: "#{reset_trigger} #{described_class.reset_trigger}",
))
end
end
describe '#reset_bot' do
before do
narrative.set_data(user, state: :tutorial_images, topic_id: topic.id)
end
context 'when trigger is initiated in a PM' do
let(:user) { Fabricate(:user) }
let(:topic) do
topic_allowed_user = Fabricate.build(:topic_allowed_user, user: user)
bot = Fabricate.build(:topic_allowed_user, user: discobot_user)
Fabricate(:private_message_topic, topic_allowed_users: [topic_allowed_user, bot])
end
let(:post) { Fabricate(:post, topic: topic) }
it 'should reset the bot' do
narrative.reset_bot(user, post)
expected_raw = I18n.t(
'discourse_narrative_bot.advanced_user_narrative.start_message',
username: user.username
)
expected_raw = <<~RAW
#{expected_raw}
#{I18n.t('discourse_narrative_bot.advanced_user_narrative.edit.instructions')}
RAW
new_post = Post.offset(1).last
expect(narrative.get_data(user)).to eq({
"topic_id" => topic.id,
"state" => "tutorial_edit",
"last_post_id" => new_post.id,
"track" => described_class.to_s,
"tutorial_edit" => {
"post_id" => Post.last.id
}
})
expect(new_post.raw).to eq(expected_raw.chomp)
expect(new_post.topic.id).to eq(topic.id)
end
end
context 'when trigger is not initiated in a PM' do
it 'should start the new track in a PM' do
narrative.reset_bot(user, other_post)
expected_raw = I18n.t(
'discourse_narrative_bot.advanced_user_narrative.start_message',
username: user.username
)
expected_raw = <<~RAW
#{expected_raw}
#{I18n.t('discourse_narrative_bot.advanced_user_narrative.edit.instructions')}
RAW
new_post = Post.offset(1).last
expect(narrative.get_data(user)).to eq({
"topic_id" => new_post.topic.id,
"state" => "tutorial_edit",
"last_post_id" => new_post.id,
"track" => described_class.to_s,
"tutorial_edit" => {
"post_id" => Post.last.id
}
})
expect(new_post.raw).to eq(expected_raw.chomp)
expect(new_post.topic.id).to_not eq(topic.id)
end
end
end
describe "#input" do
context 'edit tutorial' do
before do
narrative.set_data(user,
state: :tutorial_edit,
topic_id: topic.id,
track: described_class.to_s,
tutorial_edit: {
post_id: first_post.id
}
)
end
describe 'when post is not in the right topic' do
it 'should not do anything' do
other_post
narrative.expects(:enqueue_timeout_job).with(user).never
expect { narrative.input(:reply, user, post: other_post) }.to_not change { Post.count }
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_edit)
end
end
describe 'when user replies to the post' do
it 'should create the right reply' do
post
narrative.expects(:enqueue_timeout_job).with(user).once
expect { narrative.input(:reply, user, post: post) }
.to change { Post.count }.by(1)
expect(Post.last.raw).to eq(I18n.t(
'discourse_narrative_bot.advanced_user_narrative.edit.not_found',
url: first_post.url
))
end
describe 'when reply contains the skip trigger' do
it 'should create the right reply' do
post.update!(raw: "@#{discobot_user.username} #{skip_trigger}")
described_class.any_instance.expects(:enqueue_timeout_job).with(user)
DiscourseNarrativeBot::TrackSelector.new(:reply, user, post_id: post.id).select
new_post = Post.last
expect(new_post.raw).to eq(I18n.t(
'discourse_narrative_bot.advanced_user_narrative.delete.instructions')
)
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_delete)
end
end
end
describe 'when user edits the right post' do
let(:post_2) { Fabricate(:post, user: post.user, topic: post.topic) }
it 'should create the right reply' do
post_2
expect do
PostRevisor.new(post_2).revise!(post_2.user, raw: 'something new')
end.to change { Post.count }.by(1)
expected_raw = <<~RAW
#{I18n.t('discourse_narrative_bot.advanced_user_narrative.edit.reply')}
#{I18n.t('discourse_narrative_bot.advanced_user_narrative.delete.instructions')}
RAW
expect(Post.last.raw).to eq(expected_raw.chomp)
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_delete)
end
end
end
context 'delete tutorial' do
before do
narrative.set_data(user,
state: :tutorial_delete,
topic_id: topic.id,
track: described_class.to_s
)
end
describe 'when user replies to the topic' do
it 'should create the right reply' do
narrative.expects(:enqueue_timeout_job).with(user).once
narrative.input(:reply, user, post: post)
new_post = Post.last
expect(new_post.raw).to eq(I18n.t(
'discourse_narrative_bot.advanced_user_narrative.delete.not_found'
))
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_delete)
end
describe 'when reply contains the skip trigger' do
it 'should create the right reply' do
post.update!(raw: skip_trigger)
described_class.any_instance.expects(:enqueue_timeout_job).with(user)
DiscourseNarrativeBot::TrackSelector.new(:reply, user, post_id: post.id).select
new_post = Post.offset(1).last
expect(new_post.raw).to eq(I18n.t(
'discourse_narrative_bot.advanced_user_narrative.recover.instructions')
)
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_recover)
end
end
end
describe 'when user destroys a post in a different topic' do
it 'should not do anything' do
other_post
narrative.expects(:enqueue_timeout_job).with(user).never
PostDestroyer.new(user, other_post).destroy
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_delete)
end
end
describe 'when user deletes a post in the right topic' do
it 'should create the right reply' do
post
expect { PostDestroyer.new(user, post).destroy }
.to change { Post.count }.by(2)
expected_raw = <<~RAW
#{I18n.t('discourse_narrative_bot.advanced_user_narrative.delete.reply')}
#{I18n.t('discourse_narrative_bot.advanced_user_narrative.recover.instructions')}
RAW
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_recover)
expect(Post.offset(1).last.raw).to eq(expected_raw.chomp)
end
context 'when user is an admin' do
it 'should create the right reply' do
post
user.update!(admin: true)
expect { PostDestroyer.new(user, post).destroy }
.to_not change { Post.count }
expected_raw = <<~RAW
#{I18n.t('discourse_narrative_bot.advanced_user_narrative.delete.reply')}
#{I18n.t('discourse_narrative_bot.advanced_user_narrative.recover.instructions')}
RAW
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_recover)
expect(Post.last.raw).to eq(expected_raw.chomp)
end
end
end
end
context 'undelete post tutorial' do
before do
narrative.set_data(user,
state: :tutorial_recover,
topic_id: topic.id,
track: described_class.to_s
)
end
describe 'when user replies to the topic' do
it 'should create the right reply' do
narrative.set_data(user, narrative.get_data(user).merge(
tutorial_recover: { post_id: '1' }
))
narrative.expects(:enqueue_timeout_job).with(user).once
narrative.input(:reply, user, post: post)
new_post = Post.last
expect(new_post.raw).to eq(I18n.t(
'discourse_narrative_bot.advanced_user_narrative.recover.not_found'
))
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_recover)
end
describe 'when reply contains the skip trigger' do
it 'should create the right reply' do
parent_category = Fabricate(:category, name: 'a')
category = Fabricate(:category, parent_category: parent_category, name: 'b')
post.update!(raw: skip_trigger)
described_class.any_instance.expects(:enqueue_timeout_job).with(user)
DiscourseNarrativeBot::TrackSelector.new(:reply, user, post_id: post.id).select
new_post = Post.last
expect(new_post.raw).to eq(I18n.t(
'discourse_narrative_bot.advanced_user_narrative.category_hashtag.instructions',
category: "#a:b"
))
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_category_hashtag)
end
end
end
describe 'when user recovers a post in a different topic' do
it 'should not do anything' do
other_post
narrative.expects(:enqueue_timeout_job).with(user).never
PostDestroyer.new(user, other_post).destroy
PostDestroyer.new(user, other_post).recover
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_recover)
end
end
describe 'when user recovers a post in the right topic' do
it 'should create the right reply' do
parent_category = Fabricate(:category, name: 'a')
category = Fabricate(:category, parent_category: parent_category, name: 'b')
post
PostDestroyer.new(user, post).destroy
expect { PostDestroyer.new(user, post).recover }
.to change { Post.count }.by(1)
expected_raw = <<~RAW
#{I18n.t('discourse_narrative_bot.advanced_user_narrative.recover.reply')}
#{I18n.t('discourse_narrative_bot.advanced_user_narrative.category_hashtag.instructions', category: "#a:b")}
RAW
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_category_hashtag)
expect(Post.last.raw).to eq(expected_raw.chomp)
end
end
end
context 'category hashtag tutorial' do
before do
narrative.set_data(user,
state: :tutorial_category_hashtag,
topic_id: topic.id,
track: described_class.to_s
)
end
describe 'when post is not in the right topic' do
it 'should not do anything' do
other_post
narrative.expects(:enqueue_timeout_job).with(user).never
expect { narrative.input(:reply, user, post: other_post) }
.to_not change { Post.count }
expect(narrative.get_data(user)[:state].to_sym)
.to eq(:tutorial_category_hashtag)
end
end
describe 'when user replies to the topic' do
it 'should create the right reply' do
narrative.input(:reply, user, post: post)
new_post = Post.last
expect(new_post.raw).to eq(I18n.t(
'discourse_narrative_bot.advanced_user_narrative.category_hashtag.not_found'
))
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_category_hashtag)
end
describe 'when reply contains the skip trigger' do
it 'should create the right reply' do
post.update!(raw: skip_trigger)
described_class.any_instance.expects(:enqueue_timeout_job).with(user)
DiscourseNarrativeBot::TrackSelector.new(:reply, user, post_id: post.id).select
new_post = Post.last
expect(new_post.raw).to eq(I18n.t(
'discourse_narrative_bot.advanced_user_narrative.change_topic_notification_level.instructions'
))
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_change_topic_notification_level)
end
end
end
it 'should create the right reply' do
category = Fabricate(:category)
post.update!(raw: "Check out this ##{category.slug}")
narrative.input(:reply, user, post: post)
expected_raw = <<~RAW
#{I18n.t('discourse_narrative_bot.advanced_user_narrative.category_hashtag.reply')}
#{I18n.t('discourse_narrative_bot.advanced_user_narrative.change_topic_notification_level.instructions')}
RAW
expect(Post.last.raw).to eq(expected_raw.chomp)
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_change_topic_notification_level)
end
end
context 'topic notification level tutorial' do
before do
narrative.set_data(user,
state: :tutorial_change_topic_notification_level,
topic_id: topic.id,
track: described_class.to_s
)
end
describe 'when notification level is changed for another topic' do
it 'should not do anything' do
other_topic
user
narrative.expects(:enqueue_timeout_job).with(user).never
expect do
TopicUser.change(
user.id,
other_topic.id,
notification_level: TopicUser.notification_levels[:tracking]
)
end.to_not change { Post.count }
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_change_topic_notification_level)
end
end
describe 'when user replies to the topic' do
it 'should create the right reply' do
narrative.input(:reply, user, post: post)
new_post = Post.last
expect(new_post.raw).to eq(I18n.t(
'discourse_narrative_bot.advanced_user_narrative.change_topic_notification_level.not_found'
))
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_change_topic_notification_level)
end
describe 'when reply contains the skip trigger' do
it 'should create the right reply' do
post.update!(raw: skip_trigger)
described_class.any_instance.expects(:enqueue_timeout_job).with(user)
DiscourseNarrativeBot::TrackSelector.new(:reply, user, post_id: post.id).select
new_post = Post.last
expect(new_post.raw).to eq(I18n.t(
'discourse_narrative_bot.advanced_user_narrative.poll.instructions')
)
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_poll)
end
end
end
describe 'when user changed the topic notification level' do
it 'should create the right reply' do
TopicUser.change(
user.id,
topic.id,
notification_level: TopicUser.notification_levels[:tracking]
)
expected_raw = <<~RAW
#{I18n.t('discourse_narrative_bot.advanced_user_narrative.change_topic_notification_level.reply')}
#{I18n.t('discourse_narrative_bot.advanced_user_narrative.poll.instructions')}
RAW
expect(Post.last.raw).to eq(expected_raw.chomp)
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_poll)
end
end
end
context 'poll tutorial' do
before do
narrative.set_data(user,
state: :tutorial_poll,
topic_id: topic.id,
track: described_class.to_s
)
end
describe 'when post is not in the right topic' do
it 'should not do anything' do
other_post
narrative.expects(:enqueue_timeout_job).with(user).never
expect { narrative.input(:reply, user, post: other_post) }.to_not change { Post.count }
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_poll)
end
end
describe 'when user replies to the topic' do
it 'should create the right reply' do
narrative.input(:reply, user, post: post)
new_post = Post.last
expect(new_post.raw).to eq(I18n.t('discourse_narrative_bot.advanced_user_narrative.poll.not_found'))
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_poll)
end
describe 'when reply contains the skip trigger' do
it 'should create the right reply' do
post.update!(raw: skip_trigger)
described_class.any_instance.expects(:enqueue_timeout_job).with(user)
DiscourseNarrativeBot::TrackSelector.new(:reply, user, post_id: post.id).select
new_post = Post.last
expect(new_post.raw).to eq(I18n.t(
'discourse_narrative_bot.advanced_user_narrative.details.instructions')
)
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_details)
end
end
end
it 'should create the right reply' do
post.update!(raw: "[poll]\n* 1\n* 2\n[/poll]\n")
narrative.input(:reply, user, post: post)
expected_raw = <<~RAW
#{I18n.t('discourse_narrative_bot.advanced_user_narrative.poll.reply')}
#{I18n.t('discourse_narrative_bot.advanced_user_narrative.details.instructions')}
RAW
expect(Post.last.raw).to eq(expected_raw.chomp)
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_details)
end
end
context "details tutorial" do
before do
narrative.set_data(user,
state: :tutorial_details,
topic_id: topic.id,
track: described_class.to_s
)
end
describe 'when post is not in the right topic' do
it 'should not do anything' do
other_post
narrative.expects(:enqueue_timeout_job).with(user).never
expect { narrative.input(:reply, user, post: other_post) }.to_not change { Post.count }
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_details)
end
end
describe 'when user replies to the topic' do
it 'should create the right reply' do
narrative.input(:reply, user, post: post)
expect(Post.last.raw).to eq(I18n.t('discourse_narrative_bot.advanced_user_narrative.details.not_found'))
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_details)
end
describe 'when reply contains the skip trigger' do
it 'should create the right reply' do
post.update!(raw: skip_trigger)
expect do
DiscourseNarrativeBot::TrackSelector.new(
:reply, user, post_id: post.id
).select
end.to change { Post.count }.by(1)
expect(narrative.get_data(user)[:state].to_sym).to eq(:end)
end
end
end
it 'should create the right reply' do
post.update!(raw: "[details=\"This is a test\"]wooohoo[/details]")
narrative.input(:reply, user, post: post)
expect(Post.offset(1).last.raw).to eq(I18n.t(
'discourse_narrative_bot.advanced_user_narrative.details.reply'
))
expect(narrative.get_data(user)).to eq({
"state" => "end",
"topic_id" => topic.id,
"track" => described_class.to_s
})
expect(user.badges.where(name: DiscourseNarrativeBot::AdvancedUserNarrative::BADGE_NAME).exists?)
.to eq(true)
end
end
end
end

View File

@ -0,0 +1,895 @@
require 'rails_helper'
describe DiscourseNarrativeBot::NewUserNarrative do
let!(:welcome_topic) { Fabricate(:topic, title: 'Welcome to Discourse') }
let(:discobot_user) { User.find(-2) }
let(:first_post) { Fabricate(:post, user: discobot_user) }
let(:user) { Fabricate(:user) }
let(:topic) do
Fabricate(:private_message_topic, first_post: first_post,
topic_allowed_users: [
Fabricate.build(:topic_allowed_user, user: discobot_user),
Fabricate.build(:topic_allowed_user, user: user),
]
)
end
let(:post) { Fabricate(:post, topic: topic, user: user) }
let(:narrative) { described_class.new }
let(:other_topic) { Fabricate(:topic) }
let(:other_post) { Fabricate(:post, topic: other_topic) }
let(:profile_page_url) { "#{Discourse.base_url}/users/#{user.username}" }
let(:skip_trigger) { DiscourseNarrativeBot::TrackSelector.skip_trigger }
let(:reset_trigger) { DiscourseNarrativeBot::TrackSelector.reset_trigger }
before do
SiteSetting.discourse_narrative_bot_enabled = true
end
describe '#notify_timeout' do
before do
narrative.set_data(user,
state: :tutorial_images,
topic_id: topic.id,
last_post_id: post.id
)
end
it 'should create the right message' do
expect { narrative.notify_timeout(user) }.to change { Post.count }.by(1)
expect(Post.last.raw).to eq(I18n.t(
'discourse_narrative_bot.timeout.message',
username: user.username,
skip_trigger: skip_trigger,
reset_trigger: "#{reset_trigger} #{described_class.reset_trigger}",
))
end
end
describe '#reset_bot' do
before do
narrative.set_data(user, state: :tutorial_images, topic_id: topic.id)
end
context 'when trigger is initiated in a PM' do
let(:user) { Fabricate(:user) }
let(:topic) do
topic_allowed_user = Fabricate.build(:topic_allowed_user, user: user)
bot = Fabricate.build(:topic_allowed_user, user: discobot_user)
Fabricate(:private_message_topic, topic_allowed_users: [topic_allowed_user, bot])
end
let(:post) { Fabricate(:post, topic: topic) }
it 'should reset the bot' do
narrative.reset_bot(user, post)
expected_raw = I18n.t('discourse_narrative_bot.new_user_narrative.hello.message',
username: user.username, title: SiteSetting.title
)
expected_raw = <<~RAW
#{expected_raw}
#{I18n.t('discourse_narrative_bot.new_user_narrative.bookmark.instructions', profile_page_url: profile_page_url)}
RAW
new_post = Post.last
expect(narrative.get_data(user)).to eq({
"topic_id" => topic.id,
"state" => "tutorial_bookmark",
"last_post_id" => new_post.id,
"track" => described_class.to_s
})
expect(new_post.raw).to eq(expected_raw.chomp)
expect(new_post.topic.id).to eq(topic.id)
end
end
context 'when trigger is not initiated in a PM' do
it 'should start the new track in a PM' do
narrative.reset_bot(user, other_post)
expected_raw = I18n.t('discourse_narrative_bot.new_user_narrative.hello.message',
username: user.username, title: SiteSetting.title
)
expected_raw = <<~RAW
#{expected_raw}
#{I18n.t('discourse_narrative_bot.new_user_narrative.bookmark.instructions', profile_page_url: profile_page_url)}
RAW
new_post = Post.last
expect(narrative.get_data(user)).to eq({
"topic_id" => new_post.topic.id,
"state" => "tutorial_bookmark",
"last_post_id" => new_post.id,
"track" => described_class.to_s
})
expect(new_post.raw).to eq(expected_raw.chomp)
expect(new_post.topic.id).to_not eq(topic.id)
end
end
end
describe '#input' do
before do
SiteSetting.title = "This is an awesome site!"
narrative.set_data(user, state: :begin)
end
describe 'when an error occurs' do
before do
narrative.set_data(user, state: :tutorial_flag, topic_id: topic.id)
end
it 'should revert to the previous state' do
narrative.expects(:send).with('init_tutorial_search').raises(StandardError.new('some error'))
narrative.expects(:send).with(:reply_to_flag).returns(post)
expect { narrative.input(:flag, user, post: post) }.to raise_error(StandardError, 'some error')
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_flag)
end
end
describe 'when input does not have a valid transition from current state' do
before do
narrative.set_data(user, state: :begin)
end
it 'should raise the right error' do
expect(narrative.input(:something, user, post: post)).to eq(nil)
expect(narrative.get_data(user)[:state].to_sym).to eq(:begin)
end
end
describe 'when [:begin, :init]' do
it 'should create the right post' do
narrative.expects(:enqueue_timeout_job).never
narrative.input(:init, user, post: nil)
new_post = Post.last
expected_raw = I18n.t('discourse_narrative_bot.new_user_narrative.hello.message',
username: user.username, title: SiteSetting.title
)
expected_raw = <<~RAW
#{expected_raw}
#{I18n.t('discourse_narrative_bot.new_user_narrative.bookmark.instructions', profile_page_url: profile_page_url)}
RAW
expect(new_post.raw).to eq(expected_raw.chomp)
expect(narrative.get_data(user)[:state].to_sym)
.to eq(:tutorial_bookmark)
end
end
describe "bookmark tutorial" do
before do
narrative.set_data(user, state: :tutorial_bookmark, topic_id: topic.id)
end
describe 'when post is not in the right topic' do
it 'should not do anything' do
other_post.update!(user_id: -2)
narrative.expects(:enqueue_timeout_job).with(user).never
expect { narrative.input(:bookmark, user, post: other_post) }.to_not change { Post.count }
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_bookmark)
end
end
describe "when bookmark is not on bot's post" do
it 'should not do anything' do
narrative.expects(:enqueue_timeout_job).with(user).never
post
expect { narrative.input(:bookmark, user, post: post) }.to_not change { Post.count }
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_bookmark)
end
end
describe 'when user replies to the topic' do
it 'should create the right reply' do
narrative.expects(:enqueue_timeout_job).with(user).once
narrative.input(:reply, user, post: post)
new_post = Post.last
expect(new_post.raw).to eq(I18n.t('discourse_narrative_bot.new_user_narrative.bookmark.not_found'))
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_bookmark)
end
describe 'when reply contains the skip trigger' do
it 'should create the right reply' do
post.update!(raw: "@#{discobot_user.username} #{skip_trigger}")
described_class.any_instance.expects(:enqueue_timeout_job).with(user)
DiscourseNarrativeBot::TrackSelector.new(:reply, user, post_id: post.id).select
new_post = Post.last
expect(new_post.raw).to eq(I18n.t(
'discourse_narrative_bot.new_user_narrative.onebox.instructions')
)
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_onebox)
end
end
end
it 'should create the right reply' do
post.update!(user: described_class.discobot_user)
narrative.expects(:enqueue_timeout_job).with(user)
narrative.input(:bookmark, user, post: post)
new_post = Post.last
profile_page_url = "#{Discourse.base_url}/u/#{user.username}"
expected_raw = <<~RAW
#{I18n.t('discourse_narrative_bot.new_user_narrative.bookmark.reply', profile_page_url: profile_page_url)}
#{I18n.t('discourse_narrative_bot.new_user_narrative.onebox.instructions')}
RAW
expect(new_post.raw).to eq(expected_raw.chomp)
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_onebox)
end
end
describe 'onebox tutorial' do
before do
narrative.set_data(user, state: :tutorial_onebox, topic_id: topic.id)
end
describe 'when post is not in the right topic' do
it 'should not do anything' do
other_post
narrative.expects(:enqueue_timeout_job).with(user).never
expect { narrative.input(:reply, user, post: other_post) }.to_not change { Post.count }
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_onebox)
end
end
describe 'when post does not contain onebox' do
it 'should create the right reply' do
narrative.expects(:enqueue_timeout_job).with(user)
narrative.input(:reply, user, post: post)
new_post = Post.last
expect(new_post.raw).to eq(I18n.t('discourse_narrative_bot.new_user_narrative.onebox.not_found'))
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_onebox)
end
end
describe "when user has not liked bot's post" do
it 'should create the right reply' do
narrative.expects(:enqueue_timeout_job).with(user)
narrative.input(:reply, user, post: post)
new_post = Post.last
expect(new_post.raw).to eq(I18n.t('discourse_narrative_bot.new_user_narrative.onebox.not_found'))
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_onebox)
end
end
describe 'when user replies to the topic' do
describe 'when reply contains the skip trigger' do
it 'should create the right reply' do
post.update!(raw: skip_trigger)
described_class.any_instance.expects(:enqueue_timeout_job).with(user)
DiscourseNarrativeBot::TrackSelector.new(:reply, user, post_id: post.id).select
new_post = Post.last
expect(new_post.raw).to eq(I18n.t(
'discourse_narrative_bot.new_user_narrative.emoji.instructions')
)
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_emoji)
end
end
end
it 'should create the right reply' do
post.update!(raw: 'https://en.wikipedia.org/wiki/ROT13')
narrative.expects(:enqueue_timeout_job).with(user)
narrative.input(:reply, user, post: post)
new_post = Post.last
expected_raw = <<~RAW
#{I18n.t('discourse_narrative_bot.new_user_narrative.onebox.reply')}
#{I18n.t('discourse_narrative_bot.new_user_narrative.emoji.instructions')}
RAW
expect(new_post.raw).to eq(expected_raw.chomp)
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_emoji)
end
end
describe 'images tutorial' do
let(:post_2) { Fabricate(:post, topic: topic) }
before do
narrative.set_data(user,
state: :tutorial_images,
topic_id: topic.id,
last_post_id: post_2.id,
track: described_class.to_s
)
end
describe 'when post is not in the right topic' do
it 'should not do anything' do
other_post
narrative.expects(:enqueue_timeout_job).with(user).never
expect { narrative.input(:reply, user, post: other_post) }.to_not change { Post.count }
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_images)
end
end
describe 'when user replies to the topic' do
describe 'when reply contains the skip trigger' do
it 'should create the right reply' do
post.update!(raw: skip_trigger)
described_class.any_instance.expects(:enqueue_timeout_job).with(user)
DiscourseNarrativeBot::TrackSelector.new(:reply, user, post_id: post.id).select
new_post = Post.last
expect(new_post.raw).to eq(I18n.t(
'discourse_narrative_bot.new_user_narrative.flag.instructions',
guidelines_url: Discourse.base_url + '/guidelines',
about_url: Discourse.base_url + '/about'
))
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_flag)
end
end
end
context 'when image is not found' do
it 'should create the right replies' do
PostAction.act(user, post_2, PostActionType.types[:like])
described_class.any_instance.expects(:enqueue_timeout_job).with(user)
DiscourseNarrativeBot::TrackSelector.new(:reply, user, post_id: post.id).select
expect(Post.last.raw).to eq(I18n.t(
'discourse_narrative_bot.new_user_narrative.images.not_found',
image_url: "#{Discourse.base_url}/images/dog-walk.gif"
))
described_class.any_instance.expects(:enqueue_timeout_job).with(user)
new_post = Fabricate(:post,
user: user,
topic: topic,
raw: "<img src='https://i.ytimg.com/vi/tntOCGkgt98/maxresdefault.jpg'>"
)
DiscourseNarrativeBot::TrackSelector.new(:reply, user, post_id: new_post.id).select
expected_raw = <<~RAW
#{I18n.t('discourse_narrative_bot.new_user_narrative.images.reply')}
#{I18n.t(
'discourse_narrative_bot.new_user_narrative.flag.instructions',
guidelines_url: "#{Discourse.base_url}/guidelines",
about_url: "#{Discourse.base_url}/about"
)}
RAW
expect(Post.last.raw).to eq(expected_raw.chomp)
post_action = PostAction.last
expect(post_action.post_action_type_id).to eq(PostActionType.types[:like])
expect(post_action.user).to eq(described_class.discobot_user)
expect(post_action.post).to eq(new_post)
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_flag)
end
end
it 'should create the right replies' do
described_class.any_instance.expects(:enqueue_timeout_job).with(user)
DiscourseNarrativeBot::TrackSelector.new(:reply, user, post_id: post.id).select
expect(Post.last.raw).to eq(I18n.t(
'discourse_narrative_bot.new_user_narrative.images.not_found',
image_url: "#{Discourse.base_url}/images/dog-walk.gif"
))
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_images)
new_post = Fabricate(:post,
user: user,
topic: topic,
raw: "<img src='https://i.ytimg.com/vi/tntOCGkgt98/maxresdefault.jpg'>"
)
described_class.any_instance.expects(:enqueue_timeout_job).with(user)
DiscourseNarrativeBot::TrackSelector.new(:reply, user, post_id: new_post.id).select
expect(Post.last.raw).to eq(I18n.t(
'discourse_narrative_bot.new_user_narrative.images.like_not_found',
url: post_2.url
))
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_images)
expect(narrative.get_data(user)[:tutorial_images][:post_id])
.to eq(new_post.id)
described_class.any_instance.expects(:enqueue_timeout_job).with(user)
PostAction.act(user, post_2, PostActionType.types[:like])
expected_raw = <<~RAW
#{I18n.t('discourse_narrative_bot.new_user_narrative.images.reply')}
#{I18n.t(
'discourse_narrative_bot.new_user_narrative.flag.instructions',
guidelines_url: "#{Discourse.base_url}/guidelines",
about_url: "#{Discourse.base_url}/about"
)}
RAW
expect(Post.last.raw).to eq(expected_raw.chomp)
post_action = PostAction.last
expect(post_action.post_action_type_id).to eq(PostActionType.types[:like])
expect(post_action.user).to eq(described_class.discobot_user)
expect(post_action.post).to eq(new_post)
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_flag)
end
end
describe 'fomatting tutorial' do
before do
narrative.set_data(user, state: :tutorial_formatting, topic_id: topic.id)
end
describe 'when post is not in the right topic' do
it 'should not do anything' do
other_post
narrative.expects(:enqueue_timeout_job).with(user).never
expect { narrative.input(:reply, user, post: other_post) }.to_not change { Post.count }
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_formatting)
end
end
describe 'when post does not contain any formatting' do
it 'should create the right reply' do
narrative.expects(:enqueue_timeout_job).with(user)
narrative.input(:reply, user, post: post)
expect(Post.last.raw).to eq(I18n.t('discourse_narrative_bot.new_user_narrative.formatting.not_found'))
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_formatting)
end
end
describe 'when user replies to the topic' do
describe 'when reply contains the skip trigger' do
it 'should create the right reply' do
post.update!(raw: skip_trigger)
described_class.any_instance.expects(:enqueue_timeout_job).with(user)
DiscourseNarrativeBot::TrackSelector.new(:reply, user, post_id: post.id).select
new_post = Post.last
expect(new_post.raw).to eq(I18n.t(
'discourse_narrative_bot.new_user_narrative.quoting.instructions',
))
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_quote)
end
end
end
["**bold**", "__italic__", "[b]bold[/b]", "[i]italic[/i]"].each do |raw|
it 'should create the right reply' do
post.update!(raw: raw)
narrative.expects(:enqueue_timeout_job).with(user)
narrative.input(:reply, user, post: post)
new_post = Post.last
expected_raw = <<~RAW
#{I18n.t('discourse_narrative_bot.new_user_narrative.formatting.reply')}
#{I18n.t('discourse_narrative_bot.new_user_narrative.quoting.instructions')}
RAW
expect(new_post.raw).to eq(expected_raw.chomp)
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_quote)
end
end
end
describe 'quote tutorial' do
before do
narrative.set_data(user, state: :tutorial_quote, topic_id: topic.id)
end
describe 'when post is not in the right topic' do
it 'should not do anything' do
other_post
narrative.expects(:enqueue_timeout_job).with(user).never
expect { narrative.input(:reply, user, post: other_post) }.to_not change { Post.count }
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_quote)
end
end
describe 'when post does not contain any quotes' do
it 'should create the right reply' do
narrative.expects(:enqueue_timeout_job).with(user)
narrative.input(:reply, user, post: post)
expect(Post.last.raw).to eq(I18n.t('discourse_narrative_bot.new_user_narrative.quoting.not_found'))
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_quote)
end
end
describe 'when user replies to the topic' do
describe 'when reply contains the skip trigger' do
it 'should create the right reply' do
post.update!(raw: skip_trigger)
described_class.any_instance.expects(:enqueue_timeout_job).with(user)
DiscourseNarrativeBot::TrackSelector.new(:reply, user, post_id: post.id).select
new_post = Post.last
expect(new_post.raw).to eq(I18n.t(
'discourse_narrative_bot.new_user_narrative.images.instructions',
))
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_images)
end
end
end
it 'should create the right reply' do
post.update!(
raw: '[quote="#{post.user}, post:#{post.post_number}, topic:#{topic.id}"]\n:monkey: :fries:\n[/quote]'
)
narrative.expects(:enqueue_timeout_job).with(user)
narrative.input(:reply, user, post: post)
new_post = Post.last
expected_raw = <<~RAW
#{I18n.t('discourse_narrative_bot.new_user_narrative.quoting.reply')}
#{I18n.t('discourse_narrative_bot.new_user_narrative.images.instructions')}
RAW
expect(new_post.raw).to eq(expected_raw.chomp)
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_images)
end
end
describe 'emoji tutorial' do
before do
narrative.set_data(user, state: :tutorial_emoji, topic_id: topic.id)
end
describe 'when post is not in the right topic' do
it 'should not do anything' do
other_post
narrative.expects(:enqueue_timeout_job).with(user).never
expect { narrative.input(:reply, user, post: other_post) }.to_not change { Post.count }
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_emoji)
end
end
describe 'when post does not contain any emoji' do
it 'should create the right reply' do
narrative.expects(:enqueue_timeout_job).with(user)
narrative.input(:reply, user, post: post)
expect(Post.last.raw).to eq(I18n.t('discourse_narrative_bot.new_user_narrative.emoji.not_found'))
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_emoji)
end
end
describe 'when user replies to the topic' do
describe 'when reply contains the skip trigger' do
it 'should create the right reply' do
post.update!(raw: skip_trigger)
described_class.any_instance.expects(:enqueue_timeout_job).with(user)
DiscourseNarrativeBot::TrackSelector.new(:reply, user, post_id: post.id).select
new_post = Post.last
expect(new_post.raw).to eq(I18n.t(
'discourse_narrative_bot.new_user_narrative.mention.instructions',
discobot_username: discobot_user.username
))
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_mention)
end
end
end
it 'should create the right reply' do
post.update!(
raw: ':monkey: :fries:'
)
narrative.expects(:enqueue_timeout_job).with(user)
narrative.input(:reply, user, post: post)
new_post = Post.last
expected_raw = <<~RAW
#{I18n.t('discourse_narrative_bot.new_user_narrative.emoji.reply')}
#{I18n.t('discourse_narrative_bot.new_user_narrative.mention.instructions',
discobot_username: discobot_user.username
)}
RAW
expect(new_post.raw).to eq(expected_raw.chomp)
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_mention)
end
end
describe 'mention tutorial' do
before do
narrative.set_data(user, state: :tutorial_mention, topic_id: topic.id)
end
describe 'when post is not in the right topic' do
it 'should not do anything' do
other_post
narrative.expects(:enqueue_timeout_job).with(user).never
expect { narrative.input(:reply, user, post: other_post) }.to_not change { Post.count }
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_mention)
end
end
describe 'when post does not contain any mentions' do
it 'should create the right reply' do
narrative.expects(:enqueue_timeout_job).with(user)
narrative.input(:reply, user, post: post)
expect(Post.last.raw).to eq(I18n.t(
'discourse_narrative_bot.new_user_narrative.mention.not_found',
username: user.username,
discobot_username: discobot_user.username
))
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_mention)
end
end
describe 'when reply contains the skip trigger' do
it 'should create the right reply' do
post.update!(raw: skip_trigger)
described_class.any_instance.expects(:enqueue_timeout_job).with(user)
DiscourseNarrativeBot::TrackSelector.new(:reply, user, post_id: post.id).select
new_post = Post.last
expect(new_post.raw).to eq(I18n.t(
'discourse_narrative_bot.new_user_narrative.formatting.instructions',
discobot_username: discobot_user.username
))
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_formatting)
end
end
it 'should create the right reply' do
post.update!(
raw: '@discobot hello how are you doing today?'
)
narrative.expects(:enqueue_timeout_job).with(user)
narrative.input(:reply, user, post: post)
new_post = Post.last
expected_raw = <<~RAW
#{I18n.t('discourse_narrative_bot.new_user_narrative.mention.reply')}
#{I18n.t(
'discourse_narrative_bot.new_user_narrative.formatting.instructions'
)}
RAW
expect(new_post.raw).to eq(expected_raw.chomp)
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_formatting)
end
end
describe 'flag tutorial' do
let(:post) { Fabricate(:post, user: described_class.discobot_user, topic: topic) }
let(:flag) { Fabricate(:flag, post: post, user: user) }
let(:other_post) { Fabricate(:post, user: user, topic: topic) }
before do
flag
narrative.set_data(user, state: :tutorial_flag, topic_id: topic.id)
end
describe 'when post flagged is not for the right topic' do
it 'should not do anything' do
narrative.expects(:enqueue_timeout_job).with(user).never
flag.update!(post: other_post)
expect { narrative.input(:flag, user, post: flag.post) }.to_not change { Post.count }
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_flag)
end
end
describe 'when post being flagged does not belong to discobot ' do
it 'should not do anything' do
narrative.expects(:enqueue_timeout_job).with(user).never
flag.update!(post: other_post)
expect { narrative.input(:flag, user, post: flag.post) }.to_not change { Post.count }
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_flag)
end
end
describe 'when user replies to the topic' do
it 'should create the right reply' do
narrative.input(:reply, user, post: other_post)
new_post = Post.last
expect(new_post.raw).to eq(I18n.t('discourse_narrative_bot.new_user_narrative.flag.not_found'))
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_flag)
end
describe 'when reply contains the skip trigger' do
it 'should create the right reply' do
other_post.update!(raw: skip_trigger)
described_class.any_instance.expects(:enqueue_timeout_job).with(user)
DiscourseNarrativeBot::TrackSelector.new(:reply, user, post_id: other_post.id).select
new_post = Post.last
expect(new_post.raw).to eq(I18n.t(
'discourse_narrative_bot.new_user_narrative.search.instructions'
))
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_search)
end
end
end
it 'should create the right reply' do
narrative.expects(:enqueue_timeout_job).with(user)
expect { narrative.input(:flag, user, post: flag.post) }.to change { PostAction.count }.by(-1)
new_post = Post.last
expected_raw = <<~RAW
#{I18n.t('discourse_narrative_bot.new_user_narrative.flag.reply')}
#{I18n.t(
'discourse_narrative_bot.new_user_narrative.search.instructions'
)}
RAW
expect(new_post.raw).to eq(expected_raw.chomp)
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_search)
end
end
describe 'search tutorial' do
before do
narrative.set_data(user, state: :tutorial_search, topic_id: topic.id)
end
describe 'when post is not in the right topic' do
it 'should not do anything' do
other_post
narrative.expects(:enqueue_timeout_job).with(user).never
expect { narrative.input(:reply, user, post: other_post) }.to_not change { Post.count }
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_search)
end
end
describe 'when post does not contain the right answer' do
it 'should create the right reply' do
narrative.expects(:enqueue_timeout_job).with(user)
narrative.input(:reply, user, post: post)
expect(Post.last.raw).to eq(I18n.t(
'discourse_narrative_bot.new_user_narrative.search.not_found'
))
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_search)
end
end
describe 'when post contain the right answer' do
let(:post) { Fabricate(:post, user: described_class.discobot_user, topic: topic) }
let(:flag) { Fabricate(:flag, post: post, user: user) }
before do
narrative.set_data(user,
state: :tutorial_flag,
topic_id: topic.id
)
DiscourseNarrativeBot::TrackSelector.new(:flag, user, post_id: flag.post_id).select
expect(narrative.get_data(user)[:state].to_sym).to eq(:tutorial_search)
expect(post.reload.topic.first_post.raw).to include(I18n.t(
"discourse_narrative_bot.new_user_narrative.search.hidden_message"
))
end
it 'should clean up if the tutorial is skipped' do
post.update!(raw: skip_trigger)
expect do
DiscourseNarrativeBot::TrackSelector.new(:reply, user, post_id: post.id).select
end.to change { Post.count }.by(1)
expect(first_post.reload.raw).to eq('Hello world')
expect(narrative.get_data(user)[:state].to_sym).to eq(:end)
end
it 'should create the right reply' do
post.update!(
raw: "#{described_class::SEARCH_ANSWER} this is a capybara"
)
expect do
DiscourseNarrativeBot::TrackSelector.new(:reply, user, post_id: post.id).select
end.to change { Post.count }.by(2)
new_post = Post.offset(1).last
expect(new_post.raw).to eq(I18n.t(
'discourse_narrative_bot.new_user_narrative.search.reply',
search_url: "#{Discourse.base_url}/search"
).chomp)
expect(first_post.reload.raw).to eq('Hello world')
expect(narrative.get_data(user)).to include({
"state" => "end",
"topic_id" => new_post.topic_id,
"track" => described_class.to_s,
})
expect(user.badges.where(name: DiscourseNarrativeBot::NewUserNarrative::BADGE_NAME).exists?)
.to eq(true)
end
end
end
end
end

View File

@ -0,0 +1,28 @@
require 'rails_helper'
describe DiscourseNarrativeBot::Store do
describe '.set' do
it 'should set the right value in the plugin store' do
key = 'somekey'
described_class.set(key, 'yay')
plugin_store_row = PluginStoreRow.last
expect(plugin_store_row.value).to eq('yay')
expect(plugin_store_row.plugin_name).to eq(DiscourseNarrativeBot::PLUGIN_NAME)
expect(plugin_store_row.key).to eq(key)
end
end
describe '.get' do
it 'should get the right value from the plugin store' do
PluginStoreRow.create!(
plugin_name: DiscourseNarrativeBot::PLUGIN_NAME,
key: 'somekey',
value: 'yay',
type_name: 'string'
)
expect(described_class.get('somekey')).to eq('yay')
end
end
end

View File

@ -0,0 +1,704 @@
require 'rails_helper'
describe DiscourseNarrativeBot::TrackSelector do
let(:user) { Fabricate(:user) }
let(:discobot_user) { described_class.discobot_user }
let(:narrative) { DiscourseNarrativeBot::NewUserNarrative.new }
let(:random_mention_reply) do
I18n.t('discourse_narrative_bot.track_selector.random_mention.reply',
discobot_username: discobot_user.username,
help_trigger: described_class.help_trigger
)
end
let(:help_message) do
discobot_username = discobot_user.username
end_message = <<~RAW
#{I18n.t(
'discourse_narrative_bot.track_selector.random_mention.tracks',
discobot_username: discobot_username,
default_track: DiscourseNarrativeBot::NewUserNarrative.reset_trigger,
reset_trigger: described_class.reset_trigger,
tracks: DiscourseNarrativeBot::NewUserNarrative.reset_trigger
)}
#{I18n.t(
'discourse_narrative_bot.track_selector.random_mention.bot_actions',
discobot_username: discobot_username,
dice_trigger: described_class.dice_trigger,
quote_trigger: described_class.quote_trigger,
magic_8_ball_trigger: described_class.magic_8_ball_trigger
)}
RAW
end_message.chomp
end
describe '#select' do
context 'in a PM with discobot' do
let(:first_post) { Fabricate(:post, user: discobot_user) }
let(:topic) do
Fabricate(:private_message_topic, first_post: first_post,
topic_allowed_users: [
Fabricate.build(:topic_allowed_user, user: discobot_user),
Fabricate.build(:topic_allowed_user, user: user),
]
)
end
let(:post) { Fabricate(:post, topic: topic, user: user) }
context 'during a tutorial track' do
before do
narrative.set_data(user,
state: :tutorial_formatting,
topic_id: topic.id,
track: "DiscourseNarrativeBot::NewUserNarrative"
)
end
context 'when bot is mentioned' do
it 'should select the right track' do
post.update!(raw: '@discobot show me what you can do')
described_class.new(:reply, user, post_id: post.id).select
new_post = Post.last
expect(new_post.raw).to eq(I18n.t(
"discourse_narrative_bot.new_user_narrative.formatting.not_found"
))
end
end
context 'when bot is replied to' do
it 'should select the right track' do
post.update!(
raw: 'show me what you can do',
reply_to_post_number: first_post.post_number
)
described_class.new(:reply, user, post_id: post.id).select
expect(Post.last.raw).to eq(I18n.t(
"discourse_narrative_bot.new_user_narrative.formatting.not_found"
))
described_class.new(:reply, user, post_id: post.id).select
expected_raw = <<~RAW
#{I18n.t(
'discourse_narrative_bot.track_selector.do_not_understand.first_response',
reset_trigger: "#{described_class.reset_trigger} #{DiscourseNarrativeBot::NewUserNarrative.reset_trigger}",
)}
#{I18n.t(
'discourse_narrative_bot.track_selector.do_not_understand.track_response',
reset_trigger: "#{described_class.reset_trigger} #{DiscourseNarrativeBot::NewUserNarrative.reset_trigger}",
skip_trigger: described_class.skip_trigger
)}
RAW
expect(Post.last.raw).to eq(expected_raw.chomp)
end
end
describe 'when user thank the bot' do
it 'should like the post' do
post.update!(raw: 'thanks!')
expect { described_class.new(:reply, user, post_id: post.id).select }
.to change { PostAction.count }.by(1)
post_action = PostAction.last
expect(post_action.post).to eq(post)
expect(post_action.post_action_type_id).to eq(PostActionType.types[:like])
post = Post.last
expect(Post.last).to eq(post)
expect(DiscourseNarrativeBot::NewUserNarrative.new.get_data(user)['state'])
.to eq(nil)
end
end
context 'when reply contains a reset trigger' do
it 'should reset the track' do
post.update!(
raw: "#{described_class.reset_trigger} #{DiscourseNarrativeBot::NewUserNarrative.reset_trigger}"
)
described_class.new(:reply, user, post_id: post.id).select
expect(DiscourseNarrativeBot::NewUserNarrative.new.get_data(user)['state'])
.to eq("tutorial_bookmark")
end
describe 'reset trigger in surrounded by quotes' do
it 'should reset the track' do
post.update!(
raw: "'#{described_class.reset_trigger} #{DiscourseNarrativeBot::NewUserNarrative.reset_trigger}'"
)
described_class.new(:reply, user, post_id: post.id).select
expect(DiscourseNarrativeBot::NewUserNarrative.new.get_data(user)['state'])
.to eq("tutorial_bookmark")
end
end
describe 'reset trigger in a middle of a sentence' do
describe 'when post is less than reset trigger exact match limit' do
it 'should reset the track' do
post.update!(
raw: "I would like to #{described_class.reset_trigger} #{DiscourseNarrativeBot::NewUserNarrative.reset_trigger} now"
)
described_class.new(:reply, user, post_id: post.id).select
expect(DiscourseNarrativeBot::NewUserNarrative.new.get_data(user)['state'])
.to eq("tutorial_bookmark")
end
end
describe 'when post exceeds reset trigger exact match limit' do
it 'should not reset the track' do
post.update!(
raw: "I would like to #{described_class.reset_trigger} #{DiscourseNarrativeBot::NewUserNarrative.reset_trigger} now #{'a' * described_class::RESET_TRIGGER_EXACT_MATCH_LENGTH}"
)
expect { described_class.new(:reply, user, post_id: post.id).select }
.to change { Post.count }.by(1)
expect(DiscourseNarrativeBot::NewUserNarrative.new.get_data(user)['state'])
.to eq("tutorial_formatting")
end
end
end
context 'start/reset advanced track' do
before do
post.update!(
raw: "@#{discobot_user.username} #{described_class.reset_trigger} #{DiscourseNarrativeBot::AdvancedUserNarrative.reset_trigger}"
)
end
context 'when new user track has not been completed' do
it 'should not start the track' do
described_class.new(:reply, user, post_id: post.id).select
expect(DiscourseNarrativeBot::Store.get(user.id)['track'])
.to eq(DiscourseNarrativeBot::NewUserNarrative.to_s)
end
end
context 'when new user track has been completed' do
it 'should start the track' do
BadgeGranter.grant(
Badge.find_by(name: DiscourseNarrativeBot::NewUserNarrative::BADGE_NAME),
user
)
described_class.new(:reply, user, post_id: post.id).select
expect(DiscourseNarrativeBot::Store.get(user.id)['track'])
.to eq(DiscourseNarrativeBot::AdvancedUserNarrative.to_s)
end
end
end
end
end
context 'at the end of a tutorial track' do
before do
narrative.set_data(user,
state: :end,
topic_id: topic.id,
track: "DiscourseNarrativeBot::NewUserNarrative"
)
end
context 'generic replies' do
after do
$redis.del("#{described_class::GENERIC_REPLIES_COUNT_PREFIX}#{user.id}")
end
it 'should create the right generic do not understand responses' do
described_class.new(:reply, user, post_id: post.id).select
new_post = Post.last
expect(new_post.raw).to eq(I18n.t(
'discourse_narrative_bot.track_selector.do_not_understand.first_response',
reset_trigger: "#{described_class.reset_trigger} #{DiscourseNarrativeBot::NewUserNarrative.reset_trigger}",
))
described_class.new(:reply, user, post_id: Fabricate(:post,
topic: new_post.topic,
user: user,
reply_to_post_number: new_post.post_number
).id).select
new_post = Post.last
expect(new_post.raw).to eq(I18n.t(
'discourse_narrative_bot.track_selector.do_not_understand.second_response',
reset_trigger: "#{described_class.reset_trigger} #{DiscourseNarrativeBot::NewUserNarrative.reset_trigger}",
))
new_post = Fabricate(:post,
topic: new_post.topic,
user: user,
reply_to_post_number: new_post.post_number
)
expect { described_class.new(:reply, user, post_id: new_post.id).select }
.to_not change { Post.count }
end
end
context 'when discobot is mentioned at the end of a track' do
it 'should create the right reply' do
post.update!(raw: 'Show me what you can do @discobot')
described_class.new(:reply, user, post_id: post.id).select
new_post = Post.last
expect(new_post.raw).to eq(random_mention_reply)
end
describe 'when asking discobot for help' do
it 'should create the right reply' do
post.update!(raw: 'show me what you can do @discobot display help')
described_class.new(:reply, user, post_id: post.id).select
expect(Post.last.raw).to include(help_message)
end
describe 'as an admin or moderator' do
it 'should include the commands to start the advanced user track' do
user.update!(moderator: true)
post.update!(raw: 'Show me what you can do @discobot display help')
described_class.new(:reply, user, post_id: post.id).select
new_post = Post.last
expect(new_post.raw).to include(
DiscourseNarrativeBot::AdvancedUserNarrative.reset_trigger
)
end
end
describe 'as a user that has completed the new user track' do
it 'should include the commands to start the advanced user track' do
narrative.set_data(user,
state: :end,
topic_id: post.topic.id,
track: "DiscourseNarrativeBot::NewUserNarrative",
)
BadgeGranter.grant(
Badge.find_by(name: DiscourseNarrativeBot::NewUserNarrative::BADGE_NAME),
user
)
post.update!(raw: 'Show me what you can do @discobot display help')
described_class.new(:reply, user, post_id: post.id).select
new_post = Post.last
expect(new_post.raw).to include(
DiscourseNarrativeBot::AdvancedUserNarrative.reset_trigger
)
end
end
end
describe 'when discobot is asked to roll dice' do
before do
narrative.set_data(user,
state: :end,
topic_id: topic.id
)
end
it 'should create the right reply' do
post.update!(raw: 'roll 2d1')
described_class.new(:reply, user, post_id: post.id).select
new_post = Post.last
expect(new_post.raw).to eq(I18n.t(
"discourse_narrative_bot.dice.results", results: '1, 1'
))
end
describe 'when range of dice request is too high' do
before do
srand(1)
end
it 'should create the right reply' do
stub_request(:get, "https://www.wired.com/2016/05/mathematical-challenge-of-designing-the-worlds-most-complex-120-sided-dice")
.to_return(status: 200, body: "", headers: {})
post.update!(raw: "roll 1d#{DiscourseNarrativeBot::Dice::MAXIMUM_RANGE_OF_DICE + 1}")
described_class.new(:reply, user, post_id: post.id).select
new_post = Post.last
expected_raw = <<~RAW
#{I18n.t('discourse_narrative_bot.dice.out_of_range')}
#{I18n.t('discourse_narrative_bot.dice.results', results: '38')}
RAW
expect(new_post.raw).to eq(expected_raw.chomp)
end
end
describe 'when number of dice to roll is too high' do
it 'should create the right reply' do
post.update!(raw: "roll #{DiscourseNarrativeBot::Dice::MAXIMUM_NUM_OF_DICE + 1}d1")
described_class.new(:reply, user, post_id: post.id).select
new_post = Post.last
expected_raw = <<~RAW
#{I18n.t('discourse_narrative_bot.dice.not_enough_dice', num_of_dice: DiscourseNarrativeBot::Dice::MAXIMUM_NUM_OF_DICE)}
#{I18n.t('discourse_narrative_bot.dice.results', results: '1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1')}
RAW
expect(new_post.raw).to eq(expected_raw.chomp)
end
end
describe 'when dice combination is invalid' do
it 'should create the right reply' do
post.update!(raw: "roll 0d1")
described_class.new(:reply, user, post_id: post.id).select
expect(Post.last.raw).to eq(I18n.t(
'discourse_narrative_bot.dice.invalid'
))
end
end
end
end
end
context 'when in a normal PM with discobot' do
describe 'when discobot is replied to' do
it 'should create the right reply' do
SiteSetting.discourse_narrative_bot_disable_public_replies = true
post.update!(raw: 'Show me what you can do @discobot')
described_class.new(:reply, user, post_id: post.id).select
new_post = Post.last
expect(new_post.raw).to eq(random_mention_reply)
end
it 'should not rate limit help message' do
post.update!(raw: '@discobot')
other_post = Fabricate(:post, raw: 'discobot', topic: post.topic)
[post, other_post].each do |reply|
described_class.new(:reply, user, post_id: reply.id).select
expect(Post.last.raw).to eq(random_mention_reply)
end
end
end
end
end
context 'random discobot mentions' do
let(:topic) { Fabricate(:topic) }
let(:post) { Fabricate(:post, topic: topic, user: user) }
describe 'when discobot public replies are disabled' do
before do
SiteSetting.discourse_narrative_bot_disable_public_replies = true
end
describe 'when discobot is mentioned' do
it 'should not reply' do
post.update!(raw: 'Show me what you can do @discobot')
expect do
described_class.new(:reply, user, post_id: post.id).select
end.to_not change { Post.count }
end
end
end
describe 'when discobot is mentioned' do
it 'should create the right reply' do
post.update!(raw: 'Show me what you can do @discobot')
described_class.new(:reply, user, post_id: post.id).select
new_post = Post.last
expect(new_post.raw).to eq(random_mention_reply)
end
describe 'rate limiting random reply message in public topic' do
let(:topic) { Fabricate(:topic) }
let(:other_post) { Fabricate(:post, raw: '@discobot show me something', topic: topic) }
let(:post) { Fabricate(:post, topic: topic) }
after do
$redis.flushall
end
describe 'when random reply massage has been displayed in the last 6 hours' do
it 'should not do anything' do
$redis.set(
"#{described_class::PUBLIC_DISPLAY_BOT_HELP_KEY}:#{other_post.topic_id}",
post.post_number - 11
)
$redis.class.any_instance.expects(:ttl).returns(19.hours.to_i)
user
post.update!(raw: "Show me what you can do @discobot")
expect { described_class.new(:reply, user, post_id: post.id).select }
.to_not change { Post.count }
end
end
describe 'when random reply message has not been displayed in the last 6 hours' do
it 'should create the right reply' do
$redis.set(
"#{described_class::PUBLIC_DISPLAY_BOT_HELP_KEY}:#{other_post.topic_id}",
post.post_number - 11
)
$redis.class.any_instance.expects(:ttl).returns(7.hours.to_i)
user
post.update!(raw: "Show me what you can do @discobot")
described_class.new(:reply, user, post_id: post.id).select
expect(Post.last.raw).to eq(random_mention_reply)
end
end
describe 'when random reply message has been displayed in the last 10 replies' do
it 'should not do anything' do
described_class.new(:reply, user, post_id: other_post.id).select
expect(Post.last.raw).to eq(random_mention_reply)
expect($redis.get(
"#{described_class::PUBLIC_DISPLAY_BOT_HELP_KEY}:#{other_post.topic_id}"
).to_i).to eq(other_post.post_number.to_i)
user
post.update!(raw: "Show me what you can do @discobot")
expect do
described_class.new(:reply, user, post_id: post.id).select
end.to_not change { Post.count }
end
end
end
describe 'when asking discobot for help' do
it 'should create the right reply' do
post.update!(raw: '@discobot display help')
described_class.new(:reply, user, post_id: post.id).select
expect(Post.last.raw).to eq(help_message)
end
end
describe 'when asking discobot to start new user track' do
describe 'invalid text' do
it 'should not trigger the bot' do
post.update!(raw: '`@discobot start new user track`')
expect { described_class.new(:reply, user, post_id: post.id).select }
.to_not change { Post.count }
end
end
end
describe 'when discobot is asked to roll dice' do
it 'should create the right reply' do
post.update!(raw: '@discobot roll 2d1')
described_class.new(:reply, user, post_id: post.id).select
new_post = Post.last
expect(new_post.raw).to eq(
I18n.t("discourse_narrative_bot.dice.results",
results: '1, 1'
))
end
describe 'when dice roll is requested incorrectly' do
it 'should create the right reply' do
post.update!(raw: 'roll 2d1 @discobot')
described_class.new(:reply, user, post_id: post.id).select
expect(Post.last.raw).to eq(random_mention_reply)
end
end
describe 'when roll dice command is present inside a quote' do
it 'should ignore the command' do
user
post.update!(raw: '[quote="Donkey, post:6, topic:1"]@discobot roll 2d1[/quote]')
expect { described_class.new(:reply, user, post_id: post.id).select }
.to_not change { Post.count }
end
end
end
describe 'when a quote is requested' do
it 'should create the right reply' do
Excon.stub({ method: :get, hostname: 'api.forismatic.com' },
status: 200,
body: "{\"quoteText\":\"Be Like Water\",\"quoteAuthor\":\"Bruce Lee\"}"
)
['@discobot quote', 'hello @discobot quote there'].each do |raw|
post.update!(raw: raw)
described_class.new(:reply, user, post_id: post.id).select
new_post = Post.last
expect(new_post.raw).to eq(
I18n.t("discourse_narrative_bot.quote.results",
quote: "Be Like Water", author: "Bruce Lee"
))
end
end
describe 'when quote is requested incorrectly' do
it 'should create the right reply' do
post.update!(raw: 'quote @discobot')
described_class.new(:reply, user, post_id: post.id).select
expect(Post.last.raw).to eq(random_mention_reply)
end
end
describe 'when quote command is present inside a onebox or quote' do
it 'should ignore the command' do
user
post.update!(raw: '[quote="Donkey, post:6, topic:1"]@discobot quote[/quote]')
expect { described_class.new(:reply, user, post_id: post.id).select }
.to_not change { Post.count }
end
end
describe 'when user requesting quote has a preferred locale' do
before do
SiteSetting.allow_user_locale = true
user.update!(locale: 'it')
srand(1)
end
it 'should create the right reply' do
post.update!(raw: '@discobot quote')
described_class.new(:reply, user, post_id: post.id).select
key = "discourse_narrative_bot.quote.6"
expect(Post.last.raw).to eq(I18n.t('discourse_narrative_bot.quote.results',
quote: I18n.t("#{key}.quote"), author: I18n.t("#{key}.author")
))
end
end
end
describe 'when magic 8 ball is requested' do
before do
srand(1)
end
it 'should create the right reply' do
post.update!(raw: '@discobot fortune')
described_class.new(:reply, user, post_id: post.id).select
expect(Post.last.raw).to eq(I18n.t('discourse_narrative_bot.magic_8_ball.result',
result: I18n.t("discourse_narrative_bot.magic_8_ball.answers.6")
))
end
end
describe 'when bot is asked to reset/start a track' do
describe 'when user likes a post containing a reset trigger' do
it 'should not start the track' do
another_post = Fabricate(:post,
user: Fabricate(:user),
topic: topic,
raw: "@discobot start new user"
)
user
expect do
PostAction.act(user, another_post, PostActionType.types[:like])
end.to_not change { Post.count }
end
end
end
end
end
context 'pm to self' do
let(:other_topic) do
topic_allowed_user = Fabricate.build(:topic_allowed_user, user: user)
Fabricate(:private_message_topic, topic_allowed_users: [topic_allowed_user])
end
let(:other_post) { Fabricate(:post, topic: other_topic) }
describe 'when a new message is made' do
it 'should not do anything' do
other_post
expect { described_class.new(:reply, user, post_id: other_post.id).select }
.to_not change { Post.count }
end
end
end
context 'pms to bot' do
let(:other_topic) do
topic_allowed_user = Fabricate.build(:topic_allowed_user, user: user)
bot = Fabricate.build(:topic_allowed_user, user: discobot_user)
Fabricate(:private_message_topic, topic_allowed_users: [topic_allowed_user, bot])
end
let(:other_post) { Fabricate(:post, topic: other_topic) }
describe 'when a new like is made' do
it 'should not do anything' do
other_post
expect { described_class.new(:like, user, post_id: other_post.id).select }
.to_not change { Post.count }
end
end
describe 'when a new message is made' do
it 'should create the right reply' do
described_class.new(:reply, user, post_id: other_post.id).select
expect(Post.last.raw).to eq(random_mention_reply)
end
end
describe 'when user thanks the bot' do
it 'should like the post' do
other_post.update!(raw: 'thanks!')
expect { described_class.new(:reply, user, post_id: other_post.id).select }
.to change { PostAction.count }.by(1)
post_action = PostAction.last
expect(post_action.post).to eq(other_post)
expect(post_action.post_action_type_id).to eq(PostActionType.types[:like])
end
end
end
end
end

View File

@ -0,0 +1,33 @@
require 'rails_helper'
describe "Discobot Certificate" do
let(:user) { Fabricate(:user, name: 'Jeff Atwood') }
describe 'when viewing the certificate' do
describe 'when params are missing' do
it "should raise the right errors" do
params = {
date: Time.zone.now.strftime("%b %d %Y"),
user_id: user.id
}
params.each do |key, _|
expect { xhr :get, '/discobot/certificate.svg', params.except(key) }
.to raise_error(Discourse::InvalidParameters)
end
end
end
describe 'when date is invalid' do
it 'should raise the right error' do
expect do
xhr :get, '/discobot/certificate.svg',
name: user.name,
date: "<script type=\"text/javascript\">alert('This app is probably vulnerable to XSS attacks!');</script>",
avatar_url: 'https://somesite.com/someavatar',
user_id: user.id
end.to raise_error(ArgumentError, 'invalid date')
end
end
end
end

View File

@ -0,0 +1,26 @@
require 'rails_helper'
RSpec.describe Jobs::DiscourseNarrativeBot::GrantBadges do
let(:user) { Fabricate(:user) }
let(:other_user) { Fabricate(:user) }
before do
DiscourseNarrativeBot::Store.set(user.id, completed: [
DiscourseNarrativeBot::NewUserNarrative.to_s,
DiscourseNarrativeBot::AdvancedUserNarrative.to_s
])
end
it 'should grant the right badges' do
described_class.new.execute_onceoff({})
expect(user.badges.count).to eq(2)
expect(user.badges.map(&:name)).to eq([
DiscourseNarrativeBot::NewUserNarrative::BADGE_NAME,
DiscourseNarrativeBot::AdvancedUserNarrative::BADGE_NAME,
])
expect(other_user.badges.count).to eq(0)
end
end

View File

@ -0,0 +1,45 @@
require 'rails_helper'
RSpec.describe Jobs::SendDefaultWelcomeMessage do
let(:user) { Fabricate(:user) }
it 'should send the right welcome message' do
described_class.new.execute(user_id: user.id)
topic = Topic.last
expect(topic.title).to eq(I18n.t(
"system_messages.welcome_user.subject_template",
site_name: SiteSetting.title
))
expect(topic.first_post.raw).to eq(I18n.t(
"system_messages.welcome_user.text_body_template",
SystemMessage.new(user).defaults
).chomp)
expect(topic.closed).to eq(true)
end
describe 'for an invited user' do
let(:invite) { Fabricate(:invite, user: user, redeemed_at: Time.zone.now) }
it 'should send the right welcome message' do
described_class.new.execute(user_id: invite.user_id)
topic = Topic.last
expect(topic.title).to eq(I18n.t(
"system_messages.welcome_invite.subject_template",
site_name: SiteSetting.title
))
expect(topic.first_post.raw).to eq(I18n.t(
"system_messages.welcome_invite.text_body_template",
SystemMessage.new(user).defaults
).chomp)
expect(topic.closed).to eq(true)
end
end
end

View File

@ -0,0 +1,132 @@
require 'rails_helper'
describe User do
let(:user) { Fabricate(:user) }
let(:profile_page_url) { "#{Discourse.base_url}/users/#{user.username}" }
before do
SiteSetting.discourse_narrative_bot_enabled = true
end
describe 'when a user is created' do
it 'should initiate the bot' do
user
expected_raw = I18n.t('discourse_narrative_bot.new_user_narrative.hello.message',
username: user.username, title: SiteSetting.title
)
expect(Post.last.raw).to include(expected_raw.chomp)
end
describe 'welcome post' do
context 'disabled' do
before do
SiteSetting.disable_discourse_narrative_bot_welcome_post = true
end
it 'should not initiate the bot' do
expect { user }.to_not change { Post.count }
end
end
describe 'enabled' do
before do
SiteSetting.disable_discourse_narrative_bot_welcome_post = false
end
it 'initiate the bot' do
expect { user }.to change { Topic.count }.by(1)
expect(Topic.last.title).to eq(I18n.t(
'discourse_narrative_bot.new_user_narrative.hello.title'
))
end
describe "when send welcome message is selected" do
before do
SiteSetting.discourse_narrative_bot_welcome_post_type = 'welcome_message'
end
it 'should send the right welcome message' do
expect { user }.to change { Topic.count }.by(1)
expect(Topic.last.title).to eq(I18n.t(
"system_messages.welcome_user.subject_template",
site_name: SiteSetting.title
))
end
end
describe 'when welcome message is delayed' do
before do
SiteSetting.discourse_narrative_bot_welcome_post_delay = 100
SiteSetting.queue_jobs = true
end
it 'should delay the initialization of the new user track' do
Timecop.freeze do
user
expect(Jobs::NarrativeInit.jobs.first['at'])
.to be_within(1.second).of(Time.zone.now.to_f + 100)
end
end
it 'should delay sending the welcome message' do
SiteSetting.discourse_narrative_bot_welcome_post_type = 'welcome_message'
Timecop.freeze do
user
expect(Jobs::SendDefaultWelcomeMessage.jobs.first['at'])
.to be_within(1.second).of(Time.zone.now.to_f + 100)
end
end
end
end
end
context 'when user is staged' do
let(:user) { Fabricate(:user, staged: true) }
it 'should not initiate the bot' do
expect { user }.to_not change { Post.count }
end
end
context 'when user is anonymous?' do
let(:anonymous_user) { Fabricate(:anonymous) }
it 'should not initiate the bot' do
SiteSetting.allow_anonymous_posting = true
expect { anonymous_user }.to_not change { Post.count }
end
end
context "when user's username should be ignored" do
let(:user) { Fabricate.build(:user) }
before do
SiteSetting.discourse_narrative_bot_ignored_usernames = 'discourse|test'
end
['discourse', 'test'].each do |username|
it 'should not initiate the bot' do
expect { user.update!(username: username) }.to_not change { Post.count }
end
end
end
end
describe 'when a user has been destroyed' do
it "should clean up plugin's store" do
DiscourseNarrativeBot::Store.set(user.id, 'test')
user.destroy!
expect(DiscourseNarrativeBot::Store.get(user.id)).to eq(nil)
end
end
end